├── LICENSE ├── README.md └── src ├── firewall.py ├── functions.py ├── migrate_pfsense.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Citra IT - Excelência em TI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Migrate pfSense to OPNSense Script 2 | Script to Quickly Migrate From pfSense firewall to OPNSense With Easy 3 | 4 | ## Requirements 5 | - Install and basic setup of the target firewall (opnsense) 6 | - The interfaces names in the new Firewall *MUST MATCH* the names from the old firewall (eg.: LAN, WAN, WIFI, DMZ, MY_POKER_VLAN) 7 | - In all firewall rules or RDR (nat) must be a comment. 8 | 9 | ## Usage 10 | 1- Download this project as .zip or clone it with git clone. 11 | 2- Download pfsense config as a backup (menu diagnostics -> backup & restore), rename the downloaded file to pfsense.xml and put it in the same folder as migrate_pfsense.py file. 12 | 3- Edit the file .env to reflect your opnsense root credentals. example: 13 | ``` 14 | OPSENSE_URL = 'https://192.168.100.156' 15 | OPNSENSE_USER = 'root' 16 | OPNSENSE_PASSWORD = 'root' 17 | ``` 18 | 4- Install python3 and the project dependencies 19 | ``` 20 | python -m pip install --upgrade pip 21 | python -m pip install -r requirements.txt 22 | ``` 23 | 24 | 7- Run the script and watch the screen for processing steps. 25 | ``` 26 | python migrate_pfsense.py 27 | ``` 28 | 8- That's it! Hope it help you too. 29 | 30 | 31 | 32 | ## Limitations 33 | At the time, it's possible to migrate without problems: 34 | - Aliases 35 | - Firewall Rules (both interface rules and floating rules) 36 | - NAT rules (port forwarding) 37 | - OpenVPN servers (targeted as legacy servers) 38 | - DHCPD interface config 39 | - DHCPD static leases 40 | - Certificate Authorities and Certificates 41 | - Static Routes 42 | - Backend Authentication Servers (LDAP) 43 | - The static routes are imported with a Null Gateway. It's up to you to edit them and associate the right gateway. 44 | 45 | What's not possible to migrate yet: 46 | - Outbound NAT 47 | - 1:1 NAT 48 | - Certificate Revocation List 49 | - Local users and groups 50 | - Schedulers 51 | - Traffic Shaper 52 | - Captive Portal 53 | - PPPoE 54 | - IPSec 55 | - Unbound 56 | - HAProxy 57 | - System DNS 58 | - Hostname and Domain 59 | - Wireguard 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/firewall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import sys 4 | import json 5 | import urllib3 6 | import requests 7 | import xml.etree.ElementTree as ET 8 | import re 9 | from base64 import b64decode 10 | from bs4 import BeautifulSoup 11 | import pdb 12 | 13 | 14 | class WebClient: 15 | def __init__(self, url='', user='', password=''): 16 | self.url = url 17 | self.user = user 18 | self.password = password 19 | self.csrftoken = '' 20 | self.http_session = requests.Session() 21 | self.check_credentials() 22 | 23 | 24 | 25 | 26 | class OPNsense(WebClient): 27 | def __init__(self, url='', user='', password=''): 28 | super().__init__(url, user, password) 29 | self.form_hidden_name = '' 30 | self.form_hidden_value = '' 31 | 32 | 33 | def update_csrftoken(self, req): 34 | match = re.search(r'"X-CSRFToken", "(?P[^"]+)"', req.text) 35 | if match is not None: 36 | self.csrftoken = match.group("csrftoken") 37 | 38 | def import_hidden_values(self, req): 39 | match = re.search('input type="hidden" name="(?P[^"]+)" value="(?P[^"]+)', req.text) 40 | if match is not None: 41 | self.form_hidden_value = match.group("fieldvalue") 42 | self.form_hidden_name = match.group("fieldname") 43 | 44 | def request_get(self, endpoint): 45 | req = self.http_session.get(str(self.url) + str(endpoint), verify=False) 46 | self.update_csrftoken(req) 47 | return req 48 | 49 | def request_post(self, endpoint='/', data='', headers={}): 50 | resp = self.http_session.post( 51 | self.url + endpoint, 52 | headers=headers, 53 | data=data, 54 | verify=False, 55 | allow_redirects=False 56 | ) 57 | return resp 58 | 59 | def check_credentials(self): 60 | # request landing page 61 | r = self.request_get('/') 62 | self.import_hidden_values(r) 63 | 64 | # submitting login form 65 | login_data = {self.form_hidden_name: self.form_hidden_value, 'usernamefld': self.user, 'passwordfld': self.password, 'login':'1'} 66 | r = self.request_post('', data=login_data ) 67 | 68 | 69 | # checking if login was successful 70 | if r.status_code == 302 and 'Location' in r.headers: 71 | print(f'[+] firewall login sucessful') 72 | next_url = r.headers.get("Location") 73 | print(f'going next url: {next_url}') 74 | req = self.http_session.get(self.url + r.headers.get("Location"), verify=False ) 75 | self.update_csrftoken(req) 76 | else: 77 | print(f'[-] invalid username or password for this firewall') 78 | 79 | def list_gateways(self): 80 | pass 81 | 82 | def add_route(self, network='', gateway='"Null4"', description='', disabled='0'): 83 | #https://192.168.100.156/api/routes/routes/addroute/ 84 | #{"route":{"disabled":"0","network":"199.199.199.199/32","gateway":"01VIVO500MB_DHCP","descr":"one-nine-nine"}} 85 | route = { 86 | "route":{ 87 | "disabled": disabled, 88 | "network": network, 89 | "gateway": gateway, 90 | "descr": description 91 | } 92 | } 93 | req = self.request_post( 94 | endpoint='/api/routes/routes/addroute/', 95 | data=json.dumps(route), 96 | headers={'x-csrftoken': self.csrftoken, 'content-type': 'application/json'} 97 | ) 98 | resp = req.json() 99 | if 'result' in resp: 100 | if resp['result'] == 'saved': 101 | print(f'[+] route added successfuly') 102 | else: 103 | print(f'[-] error adding route') 104 | print(resp) 105 | else: 106 | print(f'error on request {req.request}') 107 | 108 | def get_routes(self): 109 | # https://192.168.100.156/api/routes/routes/searchroute/ 110 | # {"current":1,"rowCount":7,"sort":{},"searchPhrase":""} 111 | search_data = {"current":1,"rowCount":100,"sort":{},"searchPhrase":""} 112 | headers = {'x-csrftoken': self.csrftoken, 'content-type': 'application/json'} 113 | resp = self.request_post( 114 | endpoint='/api/routes/routes/searchroute/', 115 | data=json.dumps(search_data), 116 | headers=headers 117 | ) 118 | return resp.json()['rows'] 119 | 120 | def route_exists(self, network): 121 | my_routes = self.get_routes() 122 | for route in my_routes: 123 | if route['network'] == network: 124 | return True 125 | return False 126 | 127 | 128 | def import_ca_certificate(self, name='', crt_payload='', prv_payload=''): 129 | # https://192.168.100.156/api/trust/ca/add/ 130 | # {"ca":{"action":"existing","descr":"CA-IMPORTADA","key_type":"2048","digest":"sha256","caref":"","lifetime":"825","country":"NL","state":"","city":"","organization":"","organizationalunit":"","email":"","commonname":"","ocsp_uri":"","crt_payload":"-----BEGIN CERTIFICATE-----\n","prv_payload":"","serial":""}} 131 | ca_certificate = { 132 | "ca":{ 133 | "action": "existing", 134 | "descr": name, 135 | "crt_payload": crt_payload, 136 | "prv_payload": prv_payload 137 | } 138 | } 139 | req = self.request_post( 140 | endpoint='/api/trust/ca/add/', 141 | data=json.dumps(ca_certificate), 142 | headers={'x-csrftoken': self.csrftoken, 'content-type': 'application/json'} 143 | ) 144 | resp = req.json() 145 | if 'result' in resp: 146 | if resp['result'] == 'saved': 147 | print(f'[+] ca authority imported successfuly') 148 | else: 149 | print(f'[-] error importing ca authority') 150 | else: 151 | print(f'error on request {req.request}') 152 | 153 | def get_ca_certificates(self): 154 | # https://192.168.100.156/api/trust/ca/search/ 155 | # {"current":1,"rowCount":7,"sort":{},"searchPhrase":""} 156 | search_data = {"current":1,"rowCount":100,"sort":{},"searchPhrase":""} 157 | headers = {'x-csrftoken': self.csrftoken, 'content-type': 'application/json'} 158 | resp = self.request_post( 159 | endpoint='/api/trust/ca/search/', 160 | data=json.dumps(search_data), 161 | headers=headers 162 | ) 163 | return resp.json()['rows'] 164 | 165 | 166 | def ca_certificate_exists(self, descr): 167 | ca_list = [ x['descr'] for x in self.get_ca_certificates() ] 168 | return True if descr in ca_list else False 169 | 170 | 171 | def import_certificate(self, name='', crt_payload='', prv_payload=''): 172 | # https://192.168.100.156/api/trust/cert/add/ 173 | # {"cert":{"action":"import","descr":"CERTIFICADO","crt_payload":"","prv_payload":""}} 174 | certificate = { 175 | "cert":{ 176 | "action": "import", 177 | "descr": name, 178 | "crt_payload": crt_payload, 179 | "prv_payload": prv_payload 180 | } 181 | } 182 | req = self.request_post( 183 | endpoint='/api/trust/cert/add/', 184 | data=json.dumps(certificate), 185 | headers={'x-csrftoken': self.csrftoken, 'content-type': 'application/json'} 186 | ) 187 | resp = req.json() 188 | if 'result' in resp: 189 | if resp['result'] == 'saved': 190 | print(f'[+] certificate imported successfuly') 191 | else: 192 | print(f'[-] error importing certificate') 193 | print(resp.text) 194 | else: 195 | print(f'error on request {req.request}') 196 | 197 | 198 | def get_certificates(self): 199 | # https://192.168.100.156/api/trust/cert/search/ 200 | # {"current":1,"rowCount":7,"sort":{},"searchPhrase":""} 201 | search_data = {"current":1,"rowCount":100,"sort":{},"searchPhrase":""} 202 | headers = {'x-csrftoken': self.csrftoken, 'content-type': 'application/json'} 203 | resp = self.request_post( 204 | endpoint='/api/trust/cert/search/', 205 | data=json.dumps(search_data), 206 | headers=headers 207 | ) 208 | return resp.json()['rows'] 209 | 210 | def certificate_exists(self, descr): 211 | crt_list = [ x['descr'] for x in self.get_certificates() ] 212 | return True if descr in crt_list else False 213 | 214 | def add_auth_server(self, auth_config): 215 | req = self.request_get(endpoint='/system_authservers.php?act=new' ) 216 | data = { 217 | self.form_hidden_name: self.form_hidden_value, 218 | 'name': auth_config["name"], 219 | 'type': auth_config["type"], 220 | 'ldap_host': auth_config["host"], 221 | 'ldap_port': auth_config["ldap_port"], 222 | 'ldap_urltype': auth_config["ldap_urltype"], 223 | 'ldap_protver': auth_config["ldap_protver"], 224 | 'ldap_binddn': auth_config["ldap_binddn"], 225 | 'ldap_bindpw': auth_config["ldap_bindpw"], 226 | 'ldap_scope': auth_config["ldap_scope"], 227 | 'ldap_basedn': auth_config["ldap_basedn"], 228 | 'ldapauthcontainers': auth_config["ldap_authcn"], 229 | 'ldap_tmpltype': 'msad', 230 | 'ldap_attr_user': auth_config["ldap_attr_user"], 231 | 'save': 'Save' 232 | } 233 | 234 | # fix for when using ssl/tls ldap 235 | if auth_config["ldap_urltype"] == "SSL/TLS Encrypted": 236 | data["ldap_urltype"] = "SSL - Encrypted" 237 | elif auth_config["ldap_urltype"] == "STARTTLS Encrypted": 238 | data["ldap_urltype"] = "StartTLS" 239 | 240 | # fix for using group limited auth 241 | if auth_config["ldap_extended_enabled"] == "yes": 242 | data["ldap_extended_query"] = auth_config["ldap_extended_query"] 243 | elif auth_config["ldap_pam_groupdn"] is not None: 244 | data["ldap_extended_query"] = "memberof=%s" % (auth_config["ldap_pam_groupdn"]) 245 | 246 | headers = { 247 | 'X-CSRFToken': self.csrftoken, 248 | 'content-type': 'application/x-www-form-urlencoded' 249 | } 250 | req = self.request_post(endpoint='/system_authservers.php?act=new', data=data, headers=headers) 251 | if req.status_code == 302: 252 | print(f'[+] AUTH {data["name"]} successfuly imported!') 253 | return True 254 | else: 255 | print(f'[-] error importing authserver {data["name"]}.') 256 | print(req.text) 257 | return False 258 | 259 | def get_auth_servers(self): 260 | req = self.request_get(endpoint='/system_authservers.php') 261 | bs = BeautifulSoup(req.text, 'html.parser') 262 | auth_servers = [] 263 | for row in bs.find("table").find("tbody").find_all("tr"): 264 | for auth_server_name in row.find("td"): 265 | auth_servers.append(auth_server_name.text) 266 | return auth_servers 267 | 268 | def add_alias(self, alias): 269 | alias_data = { 270 | "alias": { 271 | "enabled": "1", 272 | "name": alias["alias"]["name"], 273 | "type": alias["alias"]["type"], 274 | "proto": "", 275 | "categories": "", 276 | "updatefreq": "", 277 | "content": alias["alias"]["content"], 278 | "interface": "", 279 | "counters": "0", 280 | "description": alias["alias"]["description"] 281 | }, 282 | "network_content":"", 283 | "authgroup_content": "" 284 | } 285 | headers = { 286 | 'X-CSRFToken': self.csrftoken, 287 | 'referer': '%s/ui/firewall/alias' % self.url, 288 | 'content-type': 'application/json' 289 | } 290 | req = self.request_post(endpoint='/api/firewall/alias/addItem/', data=json.dumps(alias_data), headers=headers) 291 | if req.status_code == 200: 292 | print(f'[+] alias {alias["alias"]["name"]} successfuly created!') 293 | return True 294 | else: 295 | print(f'[-] error registering alias {alias["alias"]["name"]}.') 296 | return False 297 | 298 | def get_aliases(self): 299 | # https://192.168.100.156/api/firewall/alias/searchItem 300 | # {"current":1,"rowCount":7,"sort":{},"searchPhrase":""} 301 | search_data = {"current":1,"rowCount":999,"sort":{},"searchPhrase":""} 302 | headers = {'x-csrftoken': self.csrftoken, 'content-type': 'application/json'} 303 | resp = self.request_post( 304 | endpoint='/api/firewall/alias/searchItem', 305 | data=json.dumps(search_data), 306 | headers=headers 307 | ) 308 | return resp.json()['rows'] 309 | 310 | def import_openvpn_server(self, openvpn_config): 311 | # format data 312 | data = { 313 | self.form_hidden_name: self.form_hidden_value, 314 | 'description': openvpn_config["description"], 315 | 'mode': openvpn_config["mode"], 316 | 'protocol': 'UDP', 317 | 'dev_mode': openvpn_config["dev_mode"], 318 | 'interface': openvpn_config["interface"], 319 | 'local_port': openvpn_config["local_port"], 320 | 'tlsmode': openvpn_config["tls_type"], 321 | 'tls': b64decode(openvpn_config["tls"]).decode(), 322 | 'caref': openvpn_config["caref"], 323 | 'crlref': openvpn_config["crlref"], 324 | 'certref': openvpn_config["certref"], 325 | 'crypto': openvpn_config["data_ciphers"].split(",")[0], 326 | 'digest': openvpn_config["digest"], 327 | 'dns_server1': openvpn_config.get('dns_server1', ''), 328 | 'cert_depth': openvpn_config["cert_depth"], 329 | 'tunnel_network': openvpn_config["tunnel_network"], 330 | 'tunnel_networkv6': '', 331 | 'local_network': openvpn_config["local_network"], 332 | 'local_networkv6': '', 333 | 'remote_network': openvpn_config["remote_network"], 334 | 'remote_networkv6': '', 335 | 'maxclients': openvpn_config["maxclients"], 336 | 'compression': openvpn_config["compression"], 337 | 'dynamic_ip': openvpn_config["dynamic_ip"], 338 | 'netbios_ntype': openvpn_config["netbios_ntype"], 339 | 'netbios_scope': openvpn_config["netbios_scope"], 340 | 'custom_options': openvpn_config["custom_options"], 341 | 'verbosity_level': openvpn_config["verbosity_level"], 342 | 'reneg-sec': '', 343 | 'save': 'Save', 344 | 'act': 'new' 345 | } 346 | 347 | # convert tunnel_network and local_network from alias to cidr 348 | aliases = self.get_aliases() 349 | for alias in aliases: 350 | if alias['name'] == data['tunnel_network']: 351 | data['tunnel_network'] = alias['content'] 352 | 353 | clean_local_network = [] 354 | for local_network in data['local_network'].split(","): 355 | bAliasFound = False 356 | for alias in aliases: 357 | if local_network.strip() == alias['name'].strip(): 358 | clean_local_network.append( alias['content'].replace("\n",",") ) 359 | bAliasFound = True 360 | break 361 | if not bAliasFound: 362 | clean_local_network.append( local_network ) 363 | data['local_network'] = ','.join(clean_local_network) 364 | 365 | 366 | # fix for dns domain registration and servers 367 | if 'dns_domain' in openvpn_config and openvpn_config['dns_domain'] is not None: 368 | data.update({ 369 | 'dns_domain_enable': 'yes', 370 | 'dns_domain': openvpn_config.get("dns_domain", ""), 371 | 'dns_server_enable': 'yes', 372 | 'push_register_dns': 'yes' 373 | }) 374 | for key in ['dns_server1', 'dns_server2', 'dns_server3', 'dns_server4']: 375 | if key in openvpn_config: 376 | data[key] = openvpn_config[key] 377 | 378 | # auth 379 | if 'authmode' in openvpn_config: 380 | data['authmode[]'] = openvpn_config["authmode"] 381 | 382 | # protocol 383 | if 'UDP' != openvpn_config["protocol"]: 384 | data["protocol"] = openvpn_config["protocol"] 385 | 386 | # use common name instead of certificate cn 387 | if openvpn_config["username_as_common_name"] == 'enabled': 388 | data['cso_login_matching'] = 'yes' 389 | 390 | headers = { 391 | 'X-CSRFToken': self.csrftoken, 392 | 'Referer': '%s/vpn_openvpn_server.php?act=new' % self.url, 393 | 'content-type': 'application/x-www-form-urlencoded' 394 | } 395 | 396 | # submit it 397 | req = self.request_post(endpoint='/vpn_openvpn_server.php?act=new', data=data, headers=headers) 398 | 399 | if req.status_code == 302: 400 | print(f'[+] OpenVPN server {data["description"]} successfuly imported!') 401 | else: 402 | print(f'error importing openvpn server {data["description"]}.') 403 | 404 | 405 | def get_ovpn_servers(self): 406 | req = self.request_get(endpoint='/vpn_openvpn_server.php') 407 | bs = BeautifulSoup(req.text, 'html.parser') 408 | ovpn_servers = [] 409 | for row in bs.find("table").find("tbody").find_all("tr"): 410 | ovpn_server = row.find_all("td")[3].text.strip() 411 | ovpn_servers.append(ovpn_server) 412 | return ovpn_servers 413 | 414 | 415 | def get_firewall_rules(self, interface=''): 416 | all_rules = [] 417 | assigned_interfaces = self.get_assigned_interfaces() 418 | if interface != '': 419 | interfaces_to_search = interface 420 | else: 421 | interfaces_to_search = list(assigned_interfaces.keys()) 422 | 423 | interface_rules = {} 424 | for interface in interfaces_to_search: 425 | interface_rules[interface] = [] 426 | if_device_name = assigned_interfaces[interface] 427 | req = self.request_get(endpoint=f'/firewall_rules.php?if={if_device_name}') 428 | bs = BeautifulSoup(req.text, 'html.parser') 429 | for rule_row in bs.find_all("td", {'class': 'rule-description'}): 430 | rule_name = rule_row.text.split("\n")[1].strip() 431 | interface_rules[interface].append(rule_name) 432 | return interface_rules 433 | 434 | 435 | def get_firewall_nat_rules(self): 436 | rdr = [] 437 | req = self.request_get(endpoint=f'/firewall_nat.php') 438 | bs = BeautifulSoup(req.text, 'html.parser') 439 | for rule_row in bs.find_all("td", {'class': 'rule-description'}): 440 | rule_name = rule_row.text.split("\n")[1].strip() 441 | rdr.append(rule_name) 442 | return rdr 443 | 444 | 445 | 446 | def get_assigned_interfaces(self): 447 | req = self.request_get(endpoint='/interfaces_assign.php') 448 | bs = BeautifulSoup(req.text, 'html.parser') 449 | interfaces = {} 450 | for row in bs.find("table").find("tbody").find_all("tr"): 451 | ifname = row.find_all("td")[0].text.strip().replace("[","").replace("]","") 452 | ifident = row.find_all("td")[1].text.strip() 453 | interfaces[ifname] = ifident 454 | return interfaces 455 | 456 | 457 | def add_filter_rule(self, rule): 458 | data = { 459 | self.form_hidden_name: self.form_hidden_value, 460 | 'type': rule["rule"]['action'], 461 | 'quick':'yes', 462 | 'interface': rule["rule"]['interface'], 463 | 'direction':'in', 464 | 'ipprotocol': rule["rule"]['ipprotocol'], 465 | 'protocol': 'any', 466 | 'src': rule["rule"]["source_net"], 467 | 'srcmask':'32', 468 | 'srcbeginport': 'any', 469 | 'srcendport': 'any', 470 | 'dst': rule["rule"]["destination_net"], 471 | 'dstmask':'32', 472 | 'dstbeginport': 'any', 473 | 'dstendport': 'any', 474 | 'descr': rule["rule"]["description"], 475 | 'sched':'', 476 | 'gateway': '', 477 | 'reply-to':'', 478 | 'set-prio':'', 479 | 'set-prio-low':'', 480 | 'prio':'', 481 | 'tos':'', 482 | 'tag':'', 483 | 'tagged':'', 484 | 'max':'', 485 | 'max-src-nodes':'', 486 | 'max-src-conn':'', 487 | 'max-src-states':'', 488 | 'max-src-conn-rate':'', 489 | 'max-src-conn-rates':'', 490 | 'overload':'virusprot', 491 | 'statetimeout':'', 492 | 'adaptivestart':'', 493 | 'adaptiveend':'', 494 | 'os':'', 495 | 'statetype':'keep state', 496 | 'Submit':'Save' 497 | } 498 | 499 | # fix for some fields that can be ommited by pfsense 500 | if 'floating' in rule["rule"]: 501 | data["floating"] = "1" 502 | if 'gateway' in rule["rule"]: 503 | data["gateway"] = rule["rule"]["gateway"] 504 | if 'protocol' in rule["rule"]: 505 | data["protocol"] = rule["rule"]["protocol"] 506 | if 'srcnot' in rule["rule"]: 507 | data["srcnot"] = rule["rule"]["srcnot"] 508 | if 'srcmask' in rule["rule"]: 509 | data["srcmask"] = rule["rule"]["srcmask"] 510 | if 'dstmask' in rule["rule"]: 511 | data["dstmask"] = rule["rule"]["dstmask"] 512 | if 'dstbeginport' in rule["rule"]: 513 | # fix for destination port as range (one field on pfsense, two separate fields in opnsense). 514 | if rule["rule"]["dstbeginport"] is not None and '-' in rule["rule"]["dstbeginport"]: 515 | data["dstbeginport"] = rule["rule"]["dstbeginport"].split("-")[0] 516 | data["dstendport"] = rule["rule"]["dstbeginport"].split("-")[1] 517 | else: 518 | data["dstbeginport"] = rule["rule"]["dstbeginport"] 519 | data["dstendport"] = rule["rule"]["dstbeginport"] 520 | 521 | 522 | 523 | 524 | #headers = r.headers 525 | headers = { 526 | 'X-CSRFToken': self.csrftoken, 527 | 'Referer': '{self.url}/firewall_rules.php?if=%s' % (data["interface"]), 528 | 'content-type': 'application/x-www-form-urlencoded' 529 | } 530 | 531 | # sending form of new rule: 532 | r = self.request_post(endpoint='/firewall_rules_edit.php?if={data["interface"]}', data=data, headers=headers) 533 | if r.status_code == 302: 534 | print(f'[+] firewall rule {data["descr"]} created!') 535 | # aplicando as alterações 536 | r = self.request_post(endpoint='/firewall_rules.php?if={data["interface"]}', data={self.form_hidden_name: self.form_hidden_value, 'act':'apply'}) 537 | if r.status_code == 200: 538 | print(f'rule {data["descr"]} applyed successfuly!') 539 | return True 540 | 541 | else: 542 | print(f'error registering rule {data["descr"]}') 543 | return False 544 | 545 | 546 | def add_nat_rule(self, nat_rule): 547 | # filling form data for new rule 548 | data = nat_rule 549 | data[self.form_hidden_name] = self.form_hidden_value 550 | 551 | 552 | #headers = r.headers 553 | headers = {} 554 | headers["X-CSRFToken"] = self.csrftoken 555 | headers["referer"] = f'{self.url}/firewall_nat_edit.php' 556 | headers["content-type"] = "application/x-www-form-urlencoded" 557 | 558 | # sending form of new rule: 559 | # !! TODO - just create the rdr, let apply later. 560 | r = self.request_post('/firewall_nat_edit.php', data=data, headers=headers) 561 | if r.status_code == 302: 562 | print(f'[+] rdr rule {data["descr"]} added with success!') 563 | return True 564 | else: 565 | print(f'[-] Error registering rdr {data["descr"]}') 566 | return False 567 | 568 | 569 | 570 | 571 | -------------------------------------------------------------------------------- /src/functions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import json 4 | import urllib3 5 | import requests 6 | import xml.etree.ElementTree as ET 7 | import re 8 | from base64 import b64decode 9 | import pdb 10 | 11 | 12 | # Function: get_pfsense_config 13 | # @Argument(pfsense_config_path) -> str : pfSense config file on filesystem 14 | # @Returns: XMLElementTree 15 | def get_pfsense_config(pfsense_config_path): 16 | tree = ET.parse(pfsense_config_path) 17 | root = tree.getroot() 18 | return root 19 | 20 | 21 | 22 | # Function: extract_rule_attributes 23 | # @Argument(el_rule) -> XMLElementTree : pfSense config rule as XMLElementTree 24 | # @Returns: Dictionary with firewall rule attributes and values 25 | def extract_rule_attributes(el_rule): 26 | rule = {} 27 | if len(el_rule) < 1: 28 | return 29 | for rule_attr in el_rule: 30 | if rule_attr.tag == 'id': 31 | rule['id'] = rule_attr.text 32 | if rule_attr.tag == 'tracker': 33 | rule['tracker'] = rule_attr.text 34 | if rule_attr.tag == 'type': 35 | rule['type'] = rule_attr.text 36 | if rule_attr.tag == 'interface': 37 | rule['interface'] = rule_attr.text 38 | if rule_attr.tag == 'ipprotocol': 39 | rule['ipprotocol'] = rule_attr.text 40 | if rule_attr.tag == 'tagged': 41 | rule['tagged'] = rule_attr.text 42 | if rule_attr.tag == 'max': 43 | rule['max'] = rule_attr.text 44 | if rule_attr.tag == 'max-src-nodes': 45 | rule['max-src-nodes'] = rule_attr.text 46 | if rule_attr.tag == 'max-src-conn': 47 | rule['max-src-conn'] = rule_attr.text 48 | if rule_attr.tag == 'max-src-conn': 49 | rule['max-src-states'] = rule_attr.text 50 | if rule_attr.tag == 'statetimeout': 51 | rule['statetimeout'] = rule_attr.text 52 | if rule_attr.tag == 'statetype': 53 | rule['statetype'] = rule_attr.text 54 | if rule_attr.tag == 'os': 55 | rule['os'] = rule_attr.text 56 | if rule_attr.tag == 'protocol': 57 | rule['protocol'] = rule_attr.text 58 | if rule_attr.tag == 'source': 59 | rule['source'] = {} 60 | rule['source']['type'] = rule_attr[0].tag 61 | rule['source']['value'] = rule_attr[0].text 62 | if len(rule_attr) > 1 and rule_attr[1].tag == 'not': 63 | rule['source']['srcnot'] = 'yes' 64 | if rule_attr.tag == 'destination': 65 | rule['destination'] = {} 66 | rule['destination']['type'] = rule_attr[0].tag 67 | rule['destination']['value'] = rule_attr[0].text 68 | if len(rule_attr) > 1: 69 | rule['destination']['dstbeginport'] = rule_attr[1].text 70 | if rule_attr.tag == 'descr': 71 | rule['descr'] = rule_attr.text 72 | if rule_attr.tag == 'associated-rule-id': 73 | rule['associated-rule-id'] = rule_attr.text 74 | if rule_attr.tag == 'gateway': 75 | rule['gateway'] = rule_attr.text 76 | if rule_attr.tag == 'floating': 77 | rule['floating'] = 'yes' 78 | 79 | return rule 80 | 81 | 82 | # Function: extract_nat_rule_attributes 83 | # @Argument(el_rule) -> XMLElementTree : pfSense config rule as XMLElementTree 84 | # @Returns: Dictionary with firewall rule attributes and values 85 | def extract_nat_rule_attributes(el_rule): 86 | rule = {} 87 | if len(el_rule) < 1: 88 | # return if this is a empty nat rule 89 | return 90 | for rule_attr in el_rule: 91 | # source 92 | if rule_attr.tag == 'source': 93 | rule['source'] = {} 94 | rule['source']['type'] = rule_attr[0].tag 95 | rule['source']['value'] = rule_attr[0].text 96 | if len(rule_attr) > 1 and rule_attr[1].tag == 'not': 97 | rule['source']['srcnot'] = 'yes' 98 | 99 | # destination and port 100 | if rule_attr.tag == 'destination': 101 | rule['destination'] = {} 102 | if len(rule_attr) > 0: 103 | # port is a range? 104 | for dst_attr in rule_attr: 105 | if dst_attr.tag == 'not': 106 | rule['destination']['dstnot'] = 'yes' 107 | elif dst_attr.tag == 'port' and '-' in dst_attr.text: 108 | rule['destination']['dstbeginport'] = dst_attr.text.split("-")[0] 109 | rule['destination']['dstendport'] = dst_attr.text.split("-")[1] 110 | elif dst_attr.tag == 'port': 111 | rule['destination']['dstbeginport'] = dst_attr.text 112 | rule['destination']['dstendport'] = dst_attr.text 113 | elif dst_attr.tag == 'network': 114 | rule['destination']['type'] = dst_attr.tag 115 | rule['destination']['value'] = dst_attr.text 116 | elif dst_attr.tag == 'address': 117 | rule['destination']['type'] = dst_attr.tag 118 | rule['destination']['value'] = dst_attr.text 119 | 120 | 121 | if rule_attr.tag == 'ipprotocol': 122 | rule['ipprotocol'] = rule_attr.text 123 | 124 | if rule_attr.tag == 'protocol': 125 | rule['protocol'] = rule_attr.text 126 | 127 | if rule_attr.tag == 'target': 128 | rule['target'] = rule_attr.text 129 | 130 | if rule_attr.tag == 'local-port': 131 | rule['local-port'] = rule_attr.text 132 | 133 | if rule_attr.tag == 'interface': 134 | rule['interface'] = rule_attr.text 135 | 136 | if rule_attr.tag == 'descr': 137 | rule['descr'] = rule_attr.text 138 | 139 | if rule_attr.tag == 'tracker': 140 | rule['tracker'] = rule_attr.text 141 | 142 | if rule_attr.tag == 'associated-rule-id': 143 | rule['associated-rule-id'] = rule_attr.text 144 | 145 | return rule 146 | 147 | 148 | 149 | 150 | # Function: extract_dhcp_attributes 151 | # @Argument(el_rule) -> XMLElementTree : pfSense config rule as XMLElementTree 152 | # @Returns: Dictionary with firewall rule attributes and values 153 | def extract_dhcp_attributes(el_dhcp): 154 | dhcp_instance_config = {} 155 | for attr in el_dhcp: 156 | if attr.tag == 'range': 157 | dhcp_instance_config['range_from'] = attr[0].text if attr[0].tag == 'from' else '' 158 | dhcp_instance_config['range_to'] = attr[1].text if attr[1].tag == 'to' else '' 159 | 160 | if attr.tag == 'enable': 161 | dhcp_instance_config["enable"] = "yes" 162 | 163 | if attr.tag == 'gateway': 164 | dhcp_instance_config["gateway"] = attr.text 165 | 166 | if attr.tag == 'domain': 167 | dhcp_instance_config["domain"] = attr.text 168 | 169 | if attr.tag == 'domainsearchlist': 170 | dhcp_instance_config["domainsearchlist"] = attr.text 171 | 172 | if attr.tag == 'dnsserver': 173 | # check if we already added a dns entry 174 | if not 'dns1' in dhcp_instance_config: 175 | dhcp_instance_config["dns1"] = attr.text 176 | elif not 'dns2' in dhcp_instance_config: 177 | dhcp_instance_config["dns2"] = attr.text 178 | else: 179 | # skip otherwise. only allow 2 dns addresses for now 180 | continue 181 | 182 | 183 | return dhcp_instance_config 184 | 185 | 186 | # Function: get_static_dhcp_leases 187 | # @Argument(el_rule) -> XMLElementTree : pfSense config rule as XMLElementTree 188 | # @Returns: Dictionary with firewall rule attributes and values 189 | def get_static_dhcp_leases(el_dhcp): 190 | dhcp_instance_config = {} 191 | static_maps = [] 192 | for attr in el_dhcp: 193 | if attr.tag == 'staticmap': 194 | attr_list = dict([(x.tag, x.text) for x in attr ]) 195 | #print(f'DEBUG: {attr_list}') 196 | lease = { 197 | 'mac': attr_list['mac'], 198 | 'cid': attr_list['cid'], 199 | 'ipaddr': attr_list['ipaddr'], 200 | 'hostname': attr_list['hostname'], 201 | 'descr': attr_list['descr'], 202 | 'gateway': '', 203 | 'domain': '', 204 | 'domainsearchlist': '', 205 | 'defaultleasetime': '', 206 | 'maxleasetime': '', 207 | 'Submit': 'Save', 208 | 'if': 'lan' 209 | 210 | } 211 | 212 | # fix for dnsservers 213 | for x in attr: 214 | if x.tag == 'dnsserver': 215 | # check if we already added a dns entry 216 | if not 'dns1' in lease: 217 | lease["dns1"] = x.text 218 | elif not 'dns2' in lease: 219 | lease["dns2"] = x.text 220 | else: 221 | # skip otherwise. only allow 2 dns addresses for now 222 | continue 223 | 224 | static_maps.append(lease) 225 | 226 | return static_maps 227 | 228 | -------------------------------------------------------------------------------- /src/migrate_pfsense.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import os 4 | import sys 5 | from pathlib import Path 6 | import json 7 | import base64 8 | import urllib3 9 | import requests 10 | import bs4 as BeautifulSoup 11 | import xml.etree.ElementTree as ET 12 | import re 13 | from dotenv import load_dotenv 14 | from firewall import OPNsense 15 | from functions import extract_rule_attributes, extract_nat_rule_attributes, \ 16 | get_pfsense_config, extract_dhcp_attributes, get_static_dhcp_leases 17 | import pdb 18 | 19 | PFSENSE_CONFIG_FILE = "pfsense.xml" 20 | 21 | if __name__ == '__main__': 22 | # disable self-signed certificates warnings 23 | urllib3.disable_warnings() 24 | 25 | # work on script dir (cwd) 26 | os.chdir(Path(__file__).parent) 27 | 28 | # load user/pass from .env 29 | load_dotenv() 30 | 31 | # connect on opnsense 32 | opn = OPNsense( 33 | url=os.environ.get("OPSENSE_URL"), 34 | user=os.environ.get("OPNSENSE_USER"), 35 | password=os.environ.get("OPNSENSE_PASSWORD") 36 | ) 37 | 38 | 39 | # -------------------------------------------------- 40 | # Load pfSense config xml 41 | #-------------------------------------------------- 42 | pfsense = get_pfsense_config(PFSENSE_CONFIG_FILE) 43 | 44 | 45 | # -------------------------------------------------- 46 | # Migrating static routes 47 | #-------------------------------------------------- 48 | # check if routes already exists, otherwise import it. 49 | # it is going to import static routes with an empty gateway, so you can then adjust accordingly. 50 | opn_routes = opn.get_routes() 51 | staticroutes = pfsense.find("staticroutes") 52 | for route in staticroutes.findall("route"): 53 | network = route.find("network").text 54 | description = route.find("descr").text 55 | if opn.route_exists(network): 56 | print(f'route already registered on target firewall. skipping it') 57 | continue 58 | else: 59 | req = opn.add_route(network=network, gateway="Null4", description=description) 60 | 61 | 62 | # -------------------------------------------------- 63 | # Migrating Certificate Authorities 64 | #-------------------------------------------------- 65 | # an interesting one. Load authorities then the certificates. 66 | # OPNsense 24.7_7 are able to import certificates and automatically associate with previously imported authorities. 67 | migrated_authorities = {} 68 | authorities = pfsense.findall("ca") 69 | for authority in authorities: 70 | crt_name = authority.find("descr").text 71 | migrated_authorities[crt_name] = {} 72 | for element in authority: 73 | migrated_authorities[crt_name][element.tag] = element.text if element.tag not in ('crt', 'prv') else '' 74 | crt_encoded = authority.find("crt").text 75 | crt = base64.b64decode(crt_encoded).decode() 76 | key_encoded = authority.find("prv").text if authority.find("prv") is not None else None 77 | key = base64.b64decode(key_encoded).decode() if key_encoded is not None else None 78 | refid = authority.find("refid").text 79 | if not opn.ca_certificate_exists(crt_name): 80 | opn.import_ca_certificate(crt_name, crt, key) 81 | else: 82 | print(f'ca {crt_name} already registered. skipping it.') 83 | 84 | 85 | # -------------------------------------------------- 86 | # Migrating Certificates 87 | #-------------------------------------------------- 88 | migrated_certificates = {} 89 | certificates = pfsense.findall("cert") 90 | for certificate in certificates: 91 | crt_name = certificate.find("descr").text 92 | migrated_certificates[crt_name] = {} 93 | for element in certificate: 94 | migrated_certificates[crt_name][element.tag] = element.text if element.tag not in ('crt', 'prv') else '' 95 | crt_encoded = certificate.find("crt").text 96 | crt = base64.b64decode(crt_encoded).decode() 97 | key_encoded = certificate.find("prv").text if authority.find("prv") is not None else None 98 | key = base64.b64decode(key_encoded).decode() if key_encoded is not None else None 99 | if not opn.certificate_exists(crt_name): 100 | opn.import_certificate(crt_name, crt, key) 101 | else: 102 | print(f'cert {crt_name} already registered. skipping it.') 103 | 104 | 105 | # -------------------------------------------------- 106 | # Migrating Auth Servers 107 | #-------------------------------------------------- 108 | existing_auth_servers = opn.get_auth_servers() 109 | system = pfsense.find("system") 110 | for authserver in system.findall("authserver"): 111 | auth_config = {} 112 | auth_server_name = authserver.find("name").text 113 | if auth_server_name in existing_auth_servers: 114 | print(f'Auth server {auth_server_name} already exists. skipping it.') 115 | continue 116 | for attr in authserver: 117 | auth_config[attr.tag] = attr.text 118 | opn.add_auth_server(auth_config) 119 | 120 | 121 | # -------------------------------------------------- 122 | # Migrating Aliases 123 | #-------------------------------------------------- 124 | existing_aliases = [alias['name'] for alias in opn.get_aliases()] 125 | aliases = pfsense.find("aliases") 126 | for alias in aliases: 127 | alias_name = alias[0].text 128 | alias_type = alias[1].text 129 | alias_value = alias[2].text 130 | alias_description = alias[3].text 131 | 132 | # abort if exists 133 | if alias_name in existing_aliases: 134 | print(f'alias {alias_name} already exists. skipping it.') 135 | continue 136 | 137 | # convert spaces (separator) into line break 138 | alias_object = { 139 | "alias": { 140 | "name": alias_name, 141 | "type":alias_type, 142 | "content": alias_value, 143 | "enabled":"1", 144 | "description":alias_description 145 | } 146 | } 147 | if alias_object["alias"]["content"] is not None: 148 | alias_object["alias"]["content"] = alias_object["alias"]["content"].replace(" ","\n") 149 | 150 | # register it 151 | opn.add_alias(alias_object) 152 | 153 | 154 | 155 | # -------------------------------------------------- 156 | # Migrating OpenVPN Servers 157 | #-------------------------------------------------- 158 | openvpn = pfsense.find("openvpn") 159 | for vpn_server in openvpn.findall("openvpn-server"): 160 | vpn_config = {} 161 | for attr in vpn_server: 162 | vpn_config[attr.tag] = attr.text 163 | ovpn_servers = opn.get_ovpn_servers() 164 | if not vpn_config['description'] in ovpn_servers: 165 | opn.import_openvpn_server(vpn_config) 166 | else: 167 | print(f'openvpn server {vpn_config["description"]} already registered. skipping it.') 168 | 169 | 170 | 171 | # -------------------------------------------------- 172 | # Migrating Firewall Rules 173 | #-------------------------------------------------- 174 | # urgh! a hard one. 175 | ifaces = opn.get_assigned_interfaces() 176 | existing_rules = opn.get_firewall_rules() 177 | filter = pfsense.find("filter") 178 | for rule in filter.findall("rule"): 179 | target_rule = extract_rule_attributes(rule) 180 | if target_rule is None: 181 | continue 182 | 183 | # get friendly interface name 184 | pfsense_interfaces = pfsense.find("interfaces") 185 | pfsense_interface_list = {} 186 | for interface in pfsense_interfaces: 187 | descr = interface.find("descr").text 188 | pfsense_interface_list[interface.tag] = { 189 | 'if': interface.find("if").text, 190 | 'descr': descr 191 | } 192 | 193 | 194 | # skip vpn rules by now. we don't support them yet. 195 | if target_rule["interface"] in ("enc0", "openvpn"): 196 | print(f"skipping ipsec/openvpn rules as we don't migrated the tunnels yet") 197 | continue 198 | 199 | # do not import existing rules 200 | if target_rule["descr"] in existing_rules[ pfsense_interface_list[target_rule['interface']]['descr'] ]: 201 | print(f'firewall rule {target_rule["descr"]} already registered. skipping it.') 202 | continue 203 | 204 | # adjust interface name as in the new firewall 205 | target_rule["interface"] = ifaces[pfsense_interface_list[target_rule['interface']]['descr']] 206 | 207 | # do not import nat-related rules (skip them) 208 | if 'associated-rule-id' in target_rule: 209 | continue 210 | 211 | # base rule attributes 212 | print(f'working on filter rule: {target_rule["descr"]}') 213 | new_rule = {"rule":{"enabled":"1","action":target_rule["type"],"quick":"1","interface":target_rule["interface"],"direction":"in","ipprotocol":target_rule.get("ipprotocol", "default_value"),"source_not":"0","destination_not":"0","log":"0","description":target_rule["descr"]}} 214 | 215 | # rules with ipv4+ipv6 address family is not supported. forcing as ipv4 216 | if target_rule.get("ipprotocol", "default_value") == 'inet46': 217 | print(f'WARN: found firewall rule set as both ipv4+ipv6 thats not compatible with opnsense. the rule will be set only for ipv4! Please, check that!') 218 | new_rule["rule"]["ipprotocol"] = "inet" 219 | 220 | # some fields may be missing from pfsense. 221 | if 'protocol' in target_rule: 222 | new_rule["rule"]["protocol"] = target_rule["protocol"] 223 | if target_rule["source"]["type"] == "any": 224 | new_rule["rule"]["source_net"] = "any" 225 | else: 226 | new_rule["rule"]["source_net"] = target_rule["source"]["value"] 227 | if 'srcnot' in target_rule["source"]: 228 | new_rule["rule"]["srcnot"] = "yes" 229 | 230 | #fix source mask 231 | if target_rule["source"]["value"] is not None and '/' in target_rule["source"]["value"]: 232 | new_rule["rule"]["source_net"] = target_rule["source"]["value"].split("/")[0] 233 | new_rule["rule"]["srcmask"] = target_rule["source"]["value"].split("/")[-1] 234 | 235 | # destination network/any 236 | if target_rule["destination"]["type"] == "any": 237 | new_rule["rule"]["destination_net"] = "any" 238 | else: 239 | new_rule["rule"]["destination_net"] = target_rule["destination"]["value"] 240 | 241 | # fix destination mask 242 | if target_rule["destination"]["value"] is not None and '/' in target_rule["destination"]["value"]: 243 | new_rule["rule"]["dstmask"] = target_rule["destination"]["value"].split("/")[-1] 244 | new_rule["rule"]["destination_net"] = target_rule["destination"]["value"].split("/")[0] 245 | 246 | # fix dstbeginport 247 | if 'dstbeginport' in target_rule["destination"]: 248 | new_rule["rule"]["dstbeginport"] = target_rule["destination"]["dstbeginport"] 249 | 250 | # fix gateway 251 | if 'gateway' in target_rule: 252 | new_rule["rule"]["gateway"] = target_rule["gateway"] 253 | 254 | # floating rule? 255 | if 'floating' in target_rule: 256 | new_rule["rule"]["floating"] = 'yes' 257 | 258 | opn.add_filter_rule(new_rule) 259 | 260 | 261 | 262 | # -------------------------------------------------- 263 | # Migrating Firewall NAT (port forwarding) 264 | #-------------------------------------------------- 265 | ifaces = opn.get_assigned_interfaces() 266 | existing_nat_rules = opn.get_firewall_nat_rules() 267 | nat_element = pfsense.find("nat") 268 | 269 | for rule_element in nat_element.findall("rule"): 270 | target_nat_rule = extract_nat_rule_attributes(rule_element) 271 | if target_nat_rule is None: 272 | continue 273 | else: 274 | print(f'\nworking on nat rule: {target_nat_rule["descr"]}') 275 | 276 | # put name as tracker if descr is empty 277 | if target_nat_rule['descr'] is None: 278 | target_nat_rule['descr'] = rule_element.find("created").find("time").text 279 | 280 | 281 | # translate interface 282 | # get friendly interfaces name 283 | pfsense_interfaces = pfsense.find("interfaces") 284 | pfsense_interface_list = {} 285 | for interface in pfsense_interfaces: 286 | descr = interface.find("descr").text 287 | pfsense_interface_list[interface.tag] = { 288 | 'if': interface.find("if").text, 289 | 'descr': descr 290 | } 291 | 292 | # translated interface 293 | nat_rule_interface = ifaces[pfsense_interface_list[target_nat_rule['interface']]['descr']] 294 | 295 | # do not import existing rules 296 | if target_nat_rule["descr"] in existing_nat_rules: 297 | print(f'firewall rule {target_rule["descr"]} already registered. skipping it.') 298 | continue 299 | 300 | # register the rule 301 | new_nat_rule = { 302 | 'interface[]': nat_rule_interface, 303 | 'ipprotocol': '', 304 | 'protocol': 'any', 305 | 'src': 'any', 306 | 'srcmask': '128', 307 | 'srcbeginport': 'any', 308 | 'srcendport': 'any', 309 | 'dst': 'any', 310 | 'dstmask': '32', 311 | 'dstbeginport': '', 312 | 'dstendport': '', 313 | 'target': target_nat_rule["target"], 314 | 'local-port': target_nat_rule["local-port"], 315 | 'descr': target_nat_rule["descr"], 316 | 'natreflection': 'default', 317 | 'filter-rule-association': 'add-associated', 318 | 'Submit': 'Save' 319 | } 320 | 321 | # fix iproto 322 | if 'ipprotocol' in target_nat_rule: 323 | new_nat_rule["ipprotocol"] = target_nat_rule["ipprotocol"] 324 | if target_nat_rule["ipprotocol"] == 'inet46': 325 | print(f'WARN: found firewall rule set as both ipv4+ipv6 thats not compatible with opnsense. the rule will be set only for ipv4! Please, check that!') 326 | new_nat_rule["ipprotocol"] = "inet" 327 | else: 328 | new_nat_rule["ipprotocol"] = "inet" 329 | 330 | # some fields may be ommited from pfsense config 331 | if 'protocol' in target_nat_rule: 332 | new_nat_rule["protocol"] = target_nat_rule["protocol"] 333 | 334 | if target_nat_rule["source"]["type"] == "any": 335 | new_nat_rule["src"] = "any" 336 | else: 337 | new_nat_rule["src"] = target_nat_rule["source"]["value"] 338 | 339 | if 'srcnot' in target_nat_rule["source"]: 340 | new_nat_rule["srcnot"] = "yes" 341 | 342 | # fix for source address with mask 343 | if target_nat_rule["source"]["value"] is not None and '/' in target_nat_rule["source"]["value"]: 344 | new_nat_rule["src"] = target_nat_rule["source"]["value"].split("/")[0] 345 | new_nat_rule["srcmask"] = target_nat_rule["source"]["value"].split("/")[-1] 346 | 347 | if target_nat_rule["destination"]["type"] == "any": 348 | new_nat_rule["dst"] = "any" 349 | else: 350 | new_nat_rule["dst"] = target_nat_rule["destination"]["value"] 351 | 352 | # fix for dest address with mask 353 | if target_nat_rule["destination"]["value"] is not None and '/' in target_nat_rule["destination"]["value"]: 354 | new_nat_rule["dst"] = target_nat_rule["destination"]["value"].split("/")[0] 355 | new_nat_rule["dstmask"] = target_nat_rule["destination"]["value"].split("/")[-1] 356 | 357 | if 'dstnot' in target_nat_rule["destination"]: 358 | new_nat_rule["dstnot"] = "yes" 359 | if 'dstbeginport' in target_nat_rule["destination"]: 360 | new_nat_rule["dstbeginport"] = target_nat_rule["destination"]["dstbeginport"] 361 | if 'dstendport' in target_nat_rule["destination"]: 362 | new_nat_rule["dstendport"] = target_nat_rule["destination"]["dstendport"] 363 | 364 | # add the new rule to opnsense 365 | opn.add_nat_rule(new_nat_rule) 366 | 367 | 368 | # click the apply button after all. 369 | headers = {} 370 | headers["X-CSRFToken"] = opn.csrftoken 371 | headers["referer"] = f'{opn.url}/firewall_nat_edit.php' 372 | headers["content-type"] = "application/x-www-form-urlencoded" 373 | req = opn.request_post('/firewall_nat.php', data={opn.form_hidden_name: opn.form_hidden_value, 'apply':'Apply changes'}, headers=headers) 374 | 375 | 376 | 377 | # -------------------------------------------------- 378 | # Migrating DHCP 379 | #-------------------------------------------------- 380 | dhcpd = pfsense.find("dhcpd") 381 | for dhcpd_interface in dhcpd: 382 | interface_config = extract_dhcp_attributes(dhcpd_interface) 383 | interface_config["if"] = dhcpd_interface.tag 384 | 385 | req = opn.request_get(f'/services_dhcp.php?if={interface_config["if"]}' ) 386 | 387 | data = interface_config 388 | data["submit"] = "Save" 389 | data[opn.form_hidden_name] = opn.form_hidden_value 390 | 391 | headers = {} 392 | headers["X-CSRFToken"] = opn.csrftoken 393 | headers["referer"] = f'{opn.url}/services_dhcp.php?if={data["if"]}' 394 | headers["content-type"] = "application/x-www-form-urlencoded" 395 | 396 | 397 | # sending form of new dhcp instance config: 398 | req = opn.request_post(f'/services_dhcp.php?if={data["if"]}', data=data, headers=headers) 399 | 400 | if req.status_code == 302: 401 | print(f'DHCP CONFIG {data["if"]} successfull imported!') 402 | else: 403 | print(f'errro importing dhcp config for interface {data["if"]}.') 404 | print(req.text) 405 | 406 | # import static leases 407 | static_leases = get_static_dhcp_leases(dhcpd_interface) 408 | for lease in static_leases: 409 | lease["if"] = interface_config["if"] 410 | req = opn.request_get(f'/services_dhcp_edit.php?if={lease["if"]}' ) 411 | 412 | data = lease 413 | data["submit"] = "Save" 414 | data[opn.form_hidden_name] = opn.form_hidden_value 415 | 416 | headers = {} 417 | headers["X-CSRFToken"] = opn.csrftoken 418 | headers["referer"] = f'/services_dhcp_edit.php?if={data["if"]}' 419 | headers["content-type"] = "application/x-www-form-urlencoded" 420 | 421 | 422 | # sending form of new static lease: 423 | req = opn.request_post(f'/services_dhcp_edit.php?if={data["if"]}', data=data, headers=headers) 424 | 425 | if req.status_code == 302: 426 | print(f'debug status: {req.headers}, {req.text}') 427 | print(f'DHCP STATIC LEASE {data["descr"]} imported!') 428 | 429 | else: 430 | print(f'error importing static dhcp lease {data["descr"]}.') 431 | print(req.text) 432 | 433 | 434 | # applying configuration 435 | headers = {} 436 | headers["X-CSRFToken"] = opn.csrftoken 437 | headers["referer"] = f'/services_dhcp_edit.php?if={data["if"]}' 438 | headers["content-type"] = "application/x-www-form-urlencoded" 439 | 440 | data = { 441 | 'apply': 'Apply changes', 442 | 'if': data["if"] 443 | } 444 | 445 | # sending apply action 446 | req = opn.request_post(f'/services_dhcp.php?if={data["if"]}', data=data, headers=headers) 447 | 448 | if req.status_code == 302: 449 | # successfuly applied configuration 450 | pass 451 | else: 452 | print(f'error applying configuration after static lease import.') 453 | 454 | 455 | 456 | 457 | 458 | print(f'\n\n===== FINISHED ========') 459 | 460 | 461 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysadminbr/migrate_pfsense/bdc4919e3758ac202985ab2aa307b7a4a16dd714/src/requirements.txt --------------------------------------------------------------------------------