Skip to content

Commit 4909c8f

Browse files
committed
geoip: T5746: Add GeoIP ASN support
1 parent a4cf50f commit 4909c8f

6 files changed

Lines changed: 134 additions & 35 deletions

File tree

interface-definitions/include/firewall/geoip.xml.i

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44
<help>GeoIP options - Data provided by DB-IP.com</help>
55
</properties>
66
<children>
7+
<leafNode name="asn">
8+
<properties>
9+
<help>Autonomous system number</help>
10+
<valueHelp>
11+
<format>u32:1-4294967294</format>
12+
<description>Autonomous system number</description>
13+
</valueHelp>
14+
<constraint>
15+
<validator name="numeric" argument="--range 1-4294967294"/>
16+
</constraint>
17+
<multi />
18+
</properties>
19+
</leafNode>
720
<leafNode name="country-code">
821
<properties>
922
<help>GeoIP country code</help>

python/vyos/firewall.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,10 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
210210
hook_name = f'name{def_suffix}'
211211
output.append(f'{ip_name} {prefix}addr {operator} @FQDN_{hook_name}_{fw_name}_{rule_id}_{prefix}')
212212

213-
if dict_search_args(side_conf, 'geoip', 'country_code'):
213+
country_code = dict_search_args(side_conf, 'geoip', 'country_code')
214+
asn = dict_search_args(side_conf, 'geoip', 'asn')
215+
if country_code or asn:
216+
geoip_prefix = 'CC' if country_code else 'ASN'
214217
operator = ''
215218
hook_name = ''
216219
if dict_search_args(side_conf, 'geoip', 'inverse_match') != None:
@@ -228,7 +231,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
228231
# for policy
229232
if hook == 'route' or hook == 'route6':
230233
hook_name = hook
231-
output.append(f'{ip_name} {prefix}addr {operator} @GEOIP_CC{def_suffix}_{hook_name}_{fw_name}_{rule_id}')
234+
output.append(f'{ip_name} {prefix}addr {operator} @GEOIP_{geoip_prefix}{def_suffix}_{hook_name}_{fw_name}_{rule_id}')
232235

233236
if 'mac_address' in side_conf:
234237
suffix = side_conf["mac_address"]

python/vyos/geoip.py

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,39 @@
1616

1717
nftables_geoip_conf = '/run/nftables-geoip.conf'
1818
dbip_database_raw = '/usr/share/vyos-geoip/dbip-country-lite.csv.gz'
19+
dbip_asn_database_raw = '/usr/share/vyos-geoip/dbip-asn-lite.csv.gz'
1920
mm_database_raw = '/usr/share/vyos-geoip/maxmind-country.zip'
21+
mm_asn_database_raw = '/usr/share/vyos-geoip/maxmind-asn.zip'
2022
geoip_database_path = '/var/cache/vyos/geoip-lookup.db'
2123
geoip_lock_file = '/var/lock/vyos-geoip.lock'
2224

2325
# Raw data
2426

2527
def geoip_download_dbip():
2628
url = 'https://download.db-ip.com/free/dbip-country-lite-{}.csv.gz'.format(strftime("%Y-%m"))
29+
asn_url = 'https://download.db-ip.com/free/dbip-asn-lite-{}.csv.gz'.format(strftime("%Y-%m"))
2730
try:
2831
dirname = os.path.dirname(dbip_database_raw)
2932
if not os.path.exists(dirname):
3033
os.mkdir(dirname)
3134

3235
download(dbip_database_raw, url)
36+
download(dbip_asn_database_raw, asn_url)
3337
return True
3438
except:
3539
return False
3640

3741
def geoip_download_maxmind(account_id : str, license_key: str, lite : bool) -> bool:
3842
db_str = 'GeoLite2' if lite else 'GeoIP2'
3943
url = f'https://{account_id}:{license_key}@download.maxmind.com/geoip/databases/{db_str}-Country-CSV/download?suffix=zip'
44+
asn_url = f'https://{account_id}:{license_key}@download.maxmind.com/geoip/databases/{db_str}-ASN-CSV/download?suffix=zip'
4045
try:
4146
dirname = os.path.dirname(mm_database_raw)
4247
if not os.path.exists(dirname):
4348
os.mkdir(dirname)
4449

4550
download(mm_database_raw, url)
51+
download(mm_asn_database_raw, asn_url)
4652
return True
4753
except:
4854
return False
@@ -68,12 +74,14 @@ def db_initialise():
6874
cur = conn.cursor()
6975
cur.execute("""
7076
CREATE TABLE IF NOT EXISTS geoip_ranges (
71-
country_code TEXT NOT NULL,
77+
country_code TEXT,
78+
asn INT,
7279
range TEXT NOT NULL,
7380
version INT NOT NULL
7481
)
7582
""")
7683
cur.execute('CREATE INDEX IF NOT EXISTS idx_cc_version ON geoip_ranges(country_code, version)')
84+
cur.execute('CREATE INDEX IF NOT EXISTS idx_asn_version ON geoip_ranges(asn, version)')
7785
conn.commit()
7886

7987
def db_import_dbip_ranges(replace=True, delete_file=False):
@@ -84,22 +92,29 @@ def db_import_dbip_ranges(replace=True, delete_file=False):
8492
return False
8593

8694
try:
87-
with gzip.open(dbip_database_raw, mode='rt') as csv_fh:
88-
reader = csv.reader(csv_fh)
95+
with sqlite3.connect(geoip_database_path) as conn:
96+
cur = conn.cursor()
8997

90-
with sqlite3.connect(geoip_database_path) as conn:
91-
cur = conn.cursor()
92-
93-
if replace:
94-
cur.execute('DELETE FROM geoip_ranges')
98+
if replace:
99+
cur.execute('DELETE FROM geoip_ranges')
95100

101+
with gzip.open(dbip_database_raw, mode='rt') as csv_fh:
102+
reader = csv.reader(csv_fh)
96103
for start, end, code in reader:
97104
version = 4 if is_ipv4(start) else 6
98105
cur.execute('INSERT INTO geoip_ranges (country_code, range, version) VALUES (?, ?, ?)', (code.lower(), f'{start}-{end}', version))
99-
conn.commit()
106+
107+
with gzip.open(dbip_asn_database_raw, mode='rt') as csv_fh:
108+
reader = csv.reader(csv_fh)
109+
for start, end, asn, _ in reader:
110+
version = 4 if is_ipv4(start) else 6
111+
cur.execute('INSERT INTO geoip_ranges (asn, range, version) VALUES (?, ?, ?)', (asn, f'{start}-{end}', version))
112+
113+
conn.commit()
100114

101115
if delete_file:
102116
os.unlink(dbip_database_raw)
117+
os.unlink(dbip_asn_database_raw)
103118

104119
return True
105120
except:
@@ -116,28 +131,28 @@ def db_import_maxmind_ranges(replace=True, delete_file=False):
116131
return False
117132

118133
try:
119-
with zipfile.ZipFile(mm_database_raw, mode='r') as zip_fh:
120-
directory = os.path.dirname(zip_fh.namelist()[0])
121-
prefix = 'GeoLite2' if any(f.startswith('GeoLite2') for f in zip_fh.namelist()) else 'GeoIP2'
134+
with sqlite3.connect(geoip_database_path) as conn:
135+
cur = conn.cursor()
122136

123-
ipv4_file = f'{directory}/{prefix}-Country-Blocks-IPv4.csv'
124-
ipv6_file = f'{directory}/{prefix}-Country-Blocks-IPv6.csv'
125-
locations_file = f'{directory}/{prefix}-Country-Locations-en.csv'
126-
locations_map = {}
137+
if replace:
138+
cur.execute('DELETE FROM geoip_ranges')
127139

128-
with zip_fh.open(locations_file) as raw_csv_fh:
129-
with TextIOWrapper(raw_csv_fh, encoding='utf-8') as csv_fh:
130-
reader = csv.DictReader(csv_fh)
140+
with zipfile.ZipFile(mm_database_raw, mode='r') as zip_fh:
141+
directory = os.path.dirname(zip_fh.namelist()[0])
142+
prefix = 'GeoLite2' if any(f.startswith('GeoLite2') for f in zip_fh.namelist()) else 'GeoIP2'
131143

132-
for row in reader:
133-
id = row['geoname_id']
134-
locations_map[id] = row['country_iso_code']
144+
ipv4_file = f'{directory}/{prefix}-Country-Blocks-IPv4.csv'
145+
ipv6_file = f'{directory}/{prefix}-Country-Blocks-IPv6.csv'
146+
locations_file = f'{directory}/{prefix}-Country-Locations-en.csv'
147+
locations_map = {}
135148

136-
with sqlite3.connect(geoip_database_path) as conn:
137-
cur = conn.cursor()
149+
with zip_fh.open(locations_file) as raw_csv_fh:
150+
with TextIOWrapper(raw_csv_fh, encoding='utf-8') as csv_fh:
151+
reader = csv.DictReader(csv_fh)
138152

139-
if replace:
140-
cur.execute('DELETE FROM geoip_ranges')
153+
for row in reader:
154+
id = row['geoname_id']
155+
locations_map[id] = row['country_iso_code']
141156

142157
with zip_fh.open(ipv4_file) as raw_csv_fh:
143158
with TextIOWrapper(raw_csv_fh, encoding='utf-8') as csv_fh:
@@ -163,16 +178,36 @@ def db_import_maxmind_ranges(replace=True, delete_file=False):
163178
code = locations_map[id]
164179
cur.execute('INSERT INTO geoip_ranges (country_code, range, version) VALUES (?, ?, 6)', (code.lower(), row['network']))
165180

166-
conn.commit()
181+
with zipfile.ZipFile(mm_asn_database_raw, mode='r') as zip_fh:
182+
directory = os.path.dirname(zip_fh.namelist()[0])
183+
prefix = 'GeoLite2' if any(f.startswith('GeoLite2') for f in zip_fh.namelist()) else 'GeoIP2'
184+
185+
ipv4_file = f'{directory}/{prefix}-ASN-Blocks-IPv4.csv'
186+
ipv6_file = f'{directory}/{prefix}-ASN-Blocks-IPv6.csv'
187+
188+
with zip_fh.open(ipv4_file) as raw_csv_fh:
189+
with TextIOWrapper(raw_csv_fh, encoding='utf-8') as csv_fh:
190+
reader = csv.DictReader(csv_fh)
191+
for row in reader:
192+
cur.execute('INSERT INTO geoip_ranges (asn, range, version) VALUES (?, ?, 4)', (row['autonomous_system_number'], row['network']))
193+
194+
with zip_fh.open(ipv6_file) as raw_csv_fh:
195+
with TextIOWrapper(raw_csv_fh, encoding='utf-8') as csv_fh:
196+
reader = csv.DictReader(csv_fh)
197+
for row in reader:
198+
cur.execute('INSERT INTO geoip_ranges (asn, range, version) VALUES (?, ?, 6)', (row['autonomous_system_number'], row['network']))
199+
200+
conn.commit()
167201

168202
if delete_file:
169203
os.unlink(mm_database_raw)
204+
os.unlink(mm_asn_database_raw)
170205

171206
return True
172207
except:
173208
return False
174209

175-
def db_return_ranges(codes, version):
210+
def db_return_cc_ranges(codes, version):
176211
out = []
177212
with sqlite3.connect(geoip_database_path) as conn:
178213
cur = conn.cursor()
@@ -181,6 +216,15 @@ def db_return_ranges(codes, version):
181216
out.append(row[0])
182217
return out
183218

219+
def db_return_asn_ranges(asn, version):
220+
out = []
221+
with sqlite3.connect(geoip_database_path) as conn:
222+
cur = conn.cursor()
223+
ph = ','.join(['?'] * len(asn))
224+
for row in cur.execute(f'SELECT range FROM geoip_ranges WHERE version = ? AND asn IN ({ph})', [version, *asn]):
225+
out.append(row[0])
226+
return out
227+
184228
# Update
185229

186230
def geoip_refresh():
@@ -220,14 +264,26 @@ def geoip_update(firewall=None, policy=None):
220264
version = 6 if path[0] == 'ipv6' else 4
221265
vprefix = '6' if version == 6 else ''
222266
set_name = f'GEOIP_CC{vprefix}_{path[1]}_{path[2]}_{path[4]}'
223-
firewall_sets[f'v{version}'][set_name] = db_return_ranges(codes, version)
267+
firewall_sets[f'v{version}'][set_name] = db_return_cc_ranges(codes, version)
268+
269+
for asns, path in dict_search_recursive(firewall, 'asn'):
270+
version = 6 if path[0] == 'ipv6' else 4
271+
vprefix = '6' if version == 6 else ''
272+
set_name = f'GEOIP_ASN{vprefix}_{path[1]}_{path[2]}_{path[4]}'
273+
firewall_sets[f'v{version}'][set_name] = db_return_asn_ranges(asns, version)
224274

225275
if policy:
226276
for codes, path in dict_search_recursive(policy, 'country_code'):
227277
version = 6 if path[0] == 'route6' else 4
228278
vprefix = '6' if version == 6 else ''
229279
set_name = f'GEOIP_CC{vprefix}_{path[0]}_{path[1]}_{path[3]}'
230-
policy_sets[f'v{version}'][set_name] = db_return_ranges(codes, version)
280+
policy_sets[f'v{version}'][set_name] = db_return_cc_ranges(codes, version)
281+
282+
for asns, path in dict_search_recursive(policy, 'asn'):
283+
version = 6 if path[0] == 'route6' else 4
284+
vprefix = '6' if version == 6 else ''
285+
set_name = f'GEOIP_ASN{vprefix}_{path[0]}_{path[1]}_{path[3]}'
286+
policy_sets[f'v{version}'][set_name] = db_return_asn_ranges(asns, version)
231287

232288
render(
233289
nftables_geoip_conf,

smoketest/scripts/cli/test_firewall.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,19 @@ def test_geoip(self):
9393
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'de'])
9494
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'fr'])
9595
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'inverse-match'])
96+
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '3', 'action', 'drop'])
97+
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '3', 'source', 'geoip', 'asn', '13335'])
98+
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '4', 'action', 'accept'])
99+
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '4', 'source', 'geoip', 'asn', '15169'])
100+
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '4', 'source', 'geoip', 'inverse-match'])
96101

97102
self.cli_commit()
98103

99104
nftables_search = [
100105
['ip saddr @GEOIP_CC_name_smoketest_1', 'drop'],
101-
['ip saddr != @GEOIP_CC_name_smoketest_2', 'accept']
106+
['ip saddr != @GEOIP_CC_name_smoketest_2', 'accept'],
107+
['ip saddr @GEOIP_ASN_name_smoketest_3', 'drop'],
108+
['ip saddr != @GEOIP_ASN_name_smoketest_4', 'accept']
102109
]
103110

104111
# -t prevents 1000+ GeoIP elements being returned

src/conf_mode/firewall.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,18 @@
8383
def geoip_sets(firewall):
8484
out = {'name': [], 'ipv6_name': []}
8585

86-
for _, path in dict_search_recursive(firewall, 'geoip'):
86+
for _, path in dict_search_recursive(firewall, 'country_code'):
8787
if (path[0] == 'ipv4'):
8888
out['name'].append(f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}')
8989
elif (path[0] == 'ipv6'):
9090
out['ipv6_name'].append(f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}')
9191

92+
for _, path in dict_search_recursive(firewall, 'asn'):
93+
if (path[0] == 'ipv4'):
94+
out['name'].append(f'GEOIP_ASN_{path[1]}_{path[2]}_{path[4]}')
95+
elif (path[0] == 'ipv6'):
96+
out['ipv6_name'].append(f'GEOIP_ASN6_{path[1]}_{path[2]}_{path[4]}')
97+
9298
return out
9399

94100
def geoip_updated(conf):
@@ -368,6 +374,10 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
368374
if len({'address', 'fqdn', 'geoip'} & set(side_conf)) > 1:
369375
raise ConfigError('Only one of address, fqdn or geoip can be specified')
370376

377+
if 'geoip' in side_conf:
378+
if len({'asn', 'country_code'} & set(side_conf['geoip'])) > 1:
379+
raise ConfigError('Only one of asn or country-code can be specified')
380+
371381
if 'group' in side_conf:
372382
if len({'address_group', 'network_group', 'domain_group', 'remote_group'} & set(side_conf['group'])) > 1:
373383
raise ConfigError('Only one address-group, network-group, remote-group or domain-group can be specified')

src/conf_mode/policy_route.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,18 @@ def geoip_updated(conf):
7575
def geoip_sets(policy):
7676
out = {'name': [], 'ipv6_name': []}
7777

78-
for _, path in dict_search_recursive(policy, 'geoip'):
78+
for _, path in dict_search_recursive(policy, 'country_code'):
7979
if (path[0] == 'route'):
8080
out['name'].append(f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}')
8181
elif (path[0] == 'route6'):
8282
out['ipv6_name'].append(f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}')
8383

84+
for _, path in dict_search_recursive(policy, 'asn'):
85+
if (path[0] == 'route'):
86+
out['name'].append(f'GEOIP_ASN_{path[0]}_{path[1]}_{path[3]}')
87+
elif (path[0] == 'route6'):
88+
out['ipv6_name'].append(f'GEOIP_ASN6_{path[0]}_{path[1]}_{path[3]}')
89+
8490
return out
8591

8692
def get_config(config=None):
@@ -152,6 +158,10 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id):
152158
if side in rule_conf:
153159
side_conf = rule_conf[side]
154160

161+
if 'geoip' in side_conf:
162+
if len({'asn', 'country_code'} & set(side_conf['geoip'])) > 1:
163+
raise ConfigError('Only one of asn or country-code can be specified')
164+
155165
if 'group' in side_conf:
156166
if len({'address_group', 'domain_group', 'network_group'} & set(side_conf['group'])) > 1:
157167
raise ConfigError('Only one address-group, domain-group or network-group can be specified')

0 commit comments

Comments
 (0)