diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml b/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml
index cfcf58c44..201947adf 100644
--- a/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml
@@ -36,6 +36,7 @@
GeoIPNetwork groupMAC address
+ BGP ASNDynamic IPv6 HostInternal (automatic)External (advanced)
diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php
index b71ceadd5..d9386560a 100644
--- a/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php
@@ -135,6 +135,24 @@ class AliasContentField extends BaseField
return $messages;
}
+ /**
+ * Validate asn alias options
+ * @param array $data to validate
+ * @return bool|Callback
+ * @throws \OPNsense\Base\ModelException
+ */
+ private function validateASN($data)
+ {
+ $messages = [];
+ $filter_opts = ["min_range" => 1, "max_range" => 4294967296];
+ foreach ($this->getItems($data) as $asn) {
+ if (filter_var($asn, FILTER_VALIDATE_INT, ["options"=> $filter_opts]) === false) {
+ $messages[] = sprintf(gettext('Entry "%s" is not a valid ASN.'), $asn);
+ }
+ }
+ return $messages;
+ }
+
/**
* Validate host options
* @param array $data to validate
@@ -335,6 +353,12 @@ class AliasContentField extends BaseField
}
]);
break;
+ case "asn":
+ $validators[] = new CallbackValidator(["callback" => function ($data) {
+ return $this->validateASN($data);
+ }
+ ]);
+ break;
default:
break;
}
diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt
index b173ca8fd..0ebeee96c 100644
--- a/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt
+++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt
@@ -234,6 +234,11 @@
$("#alias_type_geoip").show();
$("#alias\\.proto").selectpicker('show');
break;
+ case 'asn':
+ $("#alias_type_default").show();
+ $("#alias\\.proto").selectpicker('show');
+ $("#copy-paste").show();
+ break;
case 'external':
break;
case 'networkgroup':
@@ -527,6 +532,7 @@
+
diff --git a/src/opnsense/scripts/filter/lib/alias.py b/src/opnsense/scripts/filter/lib/alias.py
index f50437eee..2b3a52d2a 100755
--- a/src/opnsense/scripts/filter/lib/alias.py
+++ b/src/opnsense/scripts/filter/lib/alias.py
@@ -39,6 +39,7 @@ from dns.exception import DNSException
from . import geoip
from . import net_wildcard_iterator, AsyncDNSResolver
from .arpcache import ArpCache
+from .bgpasn import BGPASN
from .interface import InterfaceParser
class Alias(object):
@@ -279,6 +280,8 @@ class Alias(object):
return InterfaceParser(self._interface).iter_dynipv6host
elif self._type == 'mac':
return ArpCache().iter_addresses
+ elif self._type == 'asn':
+ return BGPASN(self._proto).iter_addresses
else:
return None
diff --git a/src/opnsense/scripts/filter/lib/bgpasn.py b/src/opnsense/scripts/filter/lib/bgpasn.py
new file mode 100644
index 000000000..231d43a61
--- /dev/null
+++ b/src/opnsense/scripts/filter/lib/bgpasn.py
@@ -0,0 +1,104 @@
+"""
+ Copyright (c) 2022 Ad Schellevis
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+"""
+import csv
+import fcntl
+import time
+import os
+import sys
+import syslog
+import gzip
+import requests
+
+class BGPASN:
+ _asn_source = 'https://rulesets.opnsense.org/alias/asn.gz' # source for ASN administration
+ _asn_filename = '/usr/local/share/bgp/asn.csv' # local copy
+ _asn_ttl = (86400 - 90) # validity in seconds of the local copy
+ _asn_fhandle = None # file handle to local copy
+ _asn_db = {} # cache
+
+ @classmethod
+ def _update(cls):
+ do_update = True
+ if os.path.isfile(cls._asn_filename):
+ fstat = os.stat(cls._asn_filename)
+ if (time.time() - fstat.st_mtime) < cls._asn_ttl:
+ do_update = False
+ if do_update:
+ if not os.path.exists(os.path.dirname(cls._asn_filename)):
+ os.makedirs(os.path.dirname(cls._asn_filename))
+ cls._asn_fhandle = open(cls._asn_filename, 'a+')
+ try:
+ fcntl.flock(cls._asn_fhandle, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except IOError:
+ # other process is already creating the cache, wait, let the other process do it's work and return.
+ fcntl.flock(cls._asn_fhandle, fcntl.LOCK_EX)
+ fcntl.flock(cls._asn_fhandle, fcntl.LOCK_UN)
+ return
+
+ req = requests.get(url=cls._asn_source, stream=True, timeout=20)
+ if req.status_code == 200:
+ gf = gzip.GzipFile(mode='r', fileobj=req.raw)
+ cls._asn_fhandle.seek(0)
+ cls._asn_fhandle.truncate()
+ count = 0
+ for line in gf:
+ parts = line.decode().strip().split()
+ if len(parts) == 2:
+ cls._asn_fhandle.write("%s,%s\n" % tuple(parts))
+ count += 1
+ fcntl.flock(cls._asn_fhandle, fcntl.LOCK_UN)
+ syslog.syslog(syslog.LOG_NOTICE, 'dowloaded ASN list (%d entries)' % count)
+ else:
+ syslog.syslog(
+ syslog.LOG_ERR,
+ 'error fetching BGP ASN url %s [http_code:%s]' % (cls._asn_source, req.status_code)
+ )
+ raise IOError('error fetching BGP ASN url %s' % cls._asn_source)
+ else:
+ cls._asn_fhandle = open(cls._asn_filename, 'rt')
+
+ def __init__(self, proto='IPv4'):
+ self.proto = proto.split(',')
+ if self._asn_fhandle is None:
+ # update local asn list if needed, return a file pointer to a local csv file for reading
+ self._update()
+
+ def iter_addresses(self, asn):
+ if len(self._asn_db) == 0:
+ self._asn_fhandle.seek(0)
+ for row in csv.reader(self._asn_fhandle, delimiter=',', quotechar='"'):
+ if len(row) == 2:
+ if row[1] not in self._asn_db:
+ self._asn_db[row[1]] = []
+ self._asn_db[row[1]].append(row[0])
+
+ if asn in self._asn_db:
+ for address in self._asn_db[asn]:
+ if 'IPv4' in self.proto and address.find(':') == -1:
+ yield address
+ elif 'IPv6' in self.proto and address.find(':') > -1:
+ yield address
diff --git a/src/opnsense/service/templates/OPNsense/Filter/filter_tables.conf b/src/opnsense/service/templates/OPNsense/Filter/filter_tables.conf
index e7ad48ce6..c90e1b1ae 100644
--- a/src/opnsense/service/templates/OPNsense/Filter/filter_tables.conf
+++ b/src/opnsense/service/templates/OPNsense/Filter/filter_tables.conf
@@ -28,7 +28,7 @@
{{ alias.proto|e }}
{% endif %}{% if alias.updatefreq %}
{{ alias.updatefreq|float * 86400 }}
-{% elif alias.type == 'geoip' %}
+{% elif alias.type in ['geoip', 'asn'] %}
86400
{% elif alias.type == 'host' %}
{{ system.aliasesresolveinterval|default('300') }}