Skip to content

Commit a6ffdf4

Browse files
committed
[change] OpenVPN: just use common_name as ID #94
Allow duplicate common names with duplicate_cn. Closes #94
1 parent 78eef96 commit a6ffdf4

File tree

3 files changed

+121
-25
lines changed

3 files changed

+121
-25
lines changed

README.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,27 @@ TopologyRetrievalError
389389
Raised when it is not possible to retrieve the topology data
390390
(eg: the URL might be temporary unreachable).
391391

392+
Specialized features
393+
--------------------
394+
395+
OpenVPN
396+
~~~~~~~
397+
398+
By default, the OpenVPN parser uses the common name to identify a client,
399+
this was chosen because if the public IP address is used, the same client
400+
will not be recognized if it connects with a different IP address
401+
(very probable since many ISPs use dynamic public IP addresses).
402+
403+
This does not work when the vpn server configuration allows different clients
404+
to use the same common name (which is generally not recommended anyway).
405+
406+
If you need to support legacy systems which are configured with the OpenVPN
407+
``duplicate-cn`` feature enabled, you can pass ``duplicate_cn=True`` during
408+
the initialization of ``OpenvpnParser``.
409+
This will change the behavior of the parser so that each client is identified
410+
by their common name and IP address (and additionally the port used if there
411+
are multiple clients with same common name and IP).
412+
392413
Known Issues
393414
------------
394415

netdiff/parsers/openvpn.py

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ class OpenvpnParser(BaseParser):
1111
protocol = 'OpenVPN Status Log'
1212
version = '1'
1313
metric = 'static'
14+
duplicate_cn = False
1415
# for internal use only
1516
_server_common_name = 'openvpn-server'
1617

18+
def __init__(self, *args, **kwargs):
19+
self.duplicate_cn = kwargs.pop('duplicate_cn', OpenvpnParser.duplicate_cn)
20+
super().__init__(*args, **kwargs)
21+
1722
def to_python(self, data):
1823
if not data:
1924
return None
@@ -40,14 +45,7 @@ def parse(self, data):
4045
else:
4146
clients = data.client_list.values()
4247
links = data.routing_table.values()
43-
id_list = []
44-
special_cases = []
45-
for client in clients:
46-
id_ = f'{client.common_name},{client.real_address.host}'
47-
if id_ in id_list:
48-
special_cases.append(id_)
49-
continue
50-
id_list.append(id_)
48+
special_cases = self._find_special_cases(clients)
5149
# add clients in graph as nodes
5250
for client in clients:
5351
if client.common_name == 'UNDEF':
@@ -62,6 +60,7 @@ def parse(self, data):
6260
),
6361
'bytes_received': int(client.bytes_received),
6462
'bytes_sent': int(client.bytes_sent),
63+
'common_name': client.common_name,
6564
}
6665
local_addresses = [
6766
str(route.virtual_address)
@@ -70,19 +69,57 @@ def parse(self, data):
7069
]
7170
if local_addresses:
7271
client_properties['local_addresses'] = local_addresses
73-
# if there are multiple nodes with the same common name
74-
# and host address, add the port to the node ID
75-
node_id = f'{client.common_name},{address.host}'
76-
if node_id in special_cases:
77-
node_id = f'{node_id}:{address.port}'
72+
node_id = self.get_node_id(client, special_cases)
7873
graph.add_node(node_id, **client_properties)
7974
# add links in routing table to graph
8075
for link in links:
8176
if link.common_name == 'UNDEF':
8277
continue
83-
address = link.real_address
84-
target_id = f'{link.common_name},{address.host}'
85-
if target_id in special_cases:
86-
target_id = f'{target_id}:{address.port}'
78+
target_id = self.get_target_id(link, special_cases)
8779
graph.add_edge(server, str(target_id), weight=1)
8880
return graph
81+
82+
def get_node_id(self, client, special_cases):
83+
"""
84+
when duplicate_cn is True
85+
if there are multiple nodes with the same common name
86+
and host address, add the port to the node ID
87+
when self.duplicate_cn is False:
88+
just use the common_name as node ID
89+
"""
90+
if not self.duplicate_cn:
91+
return client.common_name
92+
address = client.real_address
93+
node_id = f'{client.common_name},{address.host}'
94+
if node_id in special_cases:
95+
node_id = f'{node_id}:{address.port}'
96+
return node_id
97+
98+
def get_target_id(self, link, special_cases):
99+
"""
100+
when duplicate_cn is True
101+
if there are multiple nodes with the same common name
102+
and host address, add the port to the target ID
103+
when self.duplicate_cn is False:
104+
just use the common_name as target ID
105+
"""
106+
if not self.duplicate_cn:
107+
return link.common_name
108+
address = link.real_address
109+
target_id = f'{link.common_name},{address.host}'
110+
if target_id in special_cases:
111+
target_id = f'{target_id}:{address.port}'
112+
return target_id
113+
114+
def _find_special_cases(self, clients):
115+
if not self.duplicate_cn:
116+
return []
117+
id_list = []
118+
special_cases = []
119+
for client in clients:
120+
id_ = f'{client.common_name},{client.real_address.host}'
121+
if id_ in id_list:
122+
special_cases.append(id_)
123+
continue
124+
id_list.append(id_)
125+
return special_cases

tests/test_openvpn.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def test_label_diff_added(self):
107107
self.assertIn('nodeE', labels)
108108

109109
def test_parse_bug(self):
110-
p = OpenvpnParser(bug)
110+
p = OpenvpnParser(bug, duplicate_cn=True)
111111
data = p.json(dict=True)
112112
self.assertIsInstance(p.graph, networkx.Graph)
113113

@@ -143,14 +143,51 @@ def test_parse_bug(self):
143143
}
144144
self.assertEqual(expected, set(targets))
145145

146-
def test_parse_special_case(self):
146+
def test_parse_bug_duplicate_cn(self):
147+
p = OpenvpnParser(bug, duplicate_cn=True)
148+
data = p.json(dict=True)
149+
self.assertIsInstance(p.graph, networkx.Graph)
150+
151+
with self.subTest('Count nodes and links'):
152+
self.assertEqual(len(data['nodes']), 7)
153+
self.assertEqual(len(data['links']), 6)
154+
155+
labels = []
156+
for node in data['nodes']:
157+
labels.append(node['label'])
158+
expected = {
159+
'60c5a8fffe77607a',
160+
'60c5a8fffe77606b',
161+
'60C5A8FFFE74CB6D',
162+
'60c5a8fffe77607a',
163+
'58a0cbeffe0176d4',
164+
'58a0cbeffe0156b0',
165+
'',
166+
}
167+
with self.subTest('Check contents of nodes'):
168+
self.assertEqual(expected, set(labels))
169+
170+
targets = []
171+
for link in data['links']:
172+
targets.append(link['target'])
173+
expected = {
174+
'60c5a8fffe77607a,185.211.160.5',
175+
'60c5a8fffe77606b,185.211.160.87',
176+
'60C5A8FFFE74CB6D,194.183.10.51',
177+
'60c5a8fffe77607a,194.183.10.51',
178+
'58a0cbeffe0176d4,195.94.160.52',
179+
'58a0cbeffe0156b0,217.72.97.67',
180+
}
181+
self.assertEqual(expected, set(targets))
182+
183+
def test_parse_special_case_duplicate_cn(self):
147184
"""
148185
Tests behavior when the topology contains
149186
nodes that have the same common name and same address
150187
(it can happen when allowing reusing the same certificate
151188
and multiple clients are connected behind the same public IP)
152189
"""
153-
p = OpenvpnParser(special_case)
190+
p = OpenvpnParser(special_case, duplicate_cn=True)
154191
data = p.json(dict=True)
155192
self.assertIsInstance(p.graph, networkx.Graph)
156193
with self.subTest('Count nodes and links'):
@@ -188,9 +225,10 @@ def test_common_name_as_id(self):
188225
id_list = []
189226
for node in result['added']['nodes']:
190227
id_list.append(node['id'])
228+
self.assertEqual(node['id'], node['properties']['common_name'])
191229
self.assertEqual(len(id_list), 5)
192-
self.assertIn('nodeA,2.226.154.66', id_list)
193-
self.assertIn('nodeB,93.40.230.50', id_list)
194-
self.assertIn('nodeC,95.250.161.57', id_list)
195-
self.assertIn('nodeD,79.18.21.144', id_list)
196-
self.assertIn('nodeE,87.3.36.166', id_list)
230+
self.assertIn('nodeA', id_list)
231+
self.assertIn('nodeB', id_list)
232+
self.assertIn('nodeC', id_list)
233+
self.assertIn('nodeD', id_list)
234+
self.assertIn('nodeE', id_list)

0 commit comments

Comments
 (0)