Skip to content

Commit ab72c26

Browse files
committed
[models] Sanitize MAC addresses in called_station_id field #624
This change implements MAC address sanitization in the called_station_id field of RadiusAccounting to ensure consistent formatting across different input formats. MAC addresses are now automatically converted to lowercase colon-separated format (aa:bb:cc:dd:ee:ff) while preserving non-MAC values unchanged for RFC compliance. Changes: - Added sanitize_mac_address function in base/models.py using netaddr.EUI - Integrated automatic MAC sanitization in AbstractRadiusAccounting.save() - Updated test imports to use sanitize_mac_address from base.models - Supports multiple MAC formats: colon, dash, dot, and no separators Fixes #624
1 parent 719f7ba commit ab72c26

15 files changed

Lines changed: 564 additions & 228 deletions

docs/user/rest-api.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ Accounting
297297
/api/v1/freeradius/accounting/
298298
299299
GET
300-
...
300+
^^^
301301

302302
Returns a list of accounting objects
303303

@@ -338,7 +338,7 @@ Returns a list of accounting objects
338338
]
339339
340340
POST
341-
....
341+
^^^^
342342

343343
Add or update accounting information (start, interim-update, stop); does
344344
not return any JSON response so that freeradius will avoid processing the
@@ -374,7 +374,7 @@ framed_ip_address framed IP address
374374
===================== =====================
375375

376376
Pagination
377-
''''''''''
377+
""""""""""
378378

379379
Pagination is provided using a Link header pagination. Check `here for
380380
more information about traversing with pagination
@@ -397,7 +397,7 @@ more information about traversing with pagination
397397
parameter.
398398

399399
Filters
400-
'''''''
400+
"""""""
401401

402402
The JSON objects returned using the GET endpoint can be filtered/queried
403403
using specific parameters.
@@ -490,7 +490,7 @@ is disabled for a particular org, an empty string will be acceptable.
490490
.. _radius_registering_to_multiple_organizations:
491491

492492
Registering to Multiple Organizations
493-
.....................................
493+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
494494

495495
An **HTTP 409** response will be returned if an existing user tries to
496496
register on a URL of a different organization (because the account already

openwisp_radius/api/freeradius_views.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from .. import registration
3030
from .. import settings as app_settings
31+
from ..base.models import sanitize_mac_address
3132
from ..counters.base import BaseCounter
3233
from ..counters.exceptions import MaxQuotaReached
3334
from ..signals import radius_accounting_success
@@ -370,10 +371,12 @@ def _check_simultaneous_use(
370371
# can remain open even though the user is not authenticated
371372
# on the NAS anymore, for this reason, we shall allow re-authentication
372373
# of the same client on the same NAS.
374+
# Sanitize MAC addresses to ensure consistent comparison
375+
# since the database stores sanitized values.
373376
if called_station_id and calling_station_id:
374377
open_sessions = open_sessions.exclude(
375-
called_station_id=called_station_id,
376-
calling_station_id=calling_station_id,
378+
called_station_id=sanitize_mac_address(called_station_id),
379+
calling_station_id=sanitize_mac_address(calling_station_id),
377380
)
378381
open_sessions = open_sessions.count()
379382
if open_sessions >= max_simultaneous:

openwisp_radius/base/models.py

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -31,49 +31,6 @@
3131
from phonenumber_field.modelfields import PhoneNumberField
3232
from private_storage.fields import PrivateFileField
3333

34-
def sanitize_mac_address(mac):
35-
"""
36-
Sanitize a MAC address string to the colon-separated lowercase format.
37-
If the input is not a valid MAC address, return it unchanged.
38-
39-
Handles various MAC address formats:
40-
- 00:1A:2B:3C:4D:5E -> 00:1a:2b:3c:4d:5e
41-
- 00-1A-2B-3C-4D-5E -> 00:1a:2b:3c:4d:5e
42-
- 001A.2B3C.4D5E -> 00:1a:2b:3c:4d:5e
43-
- 001A2B3C4D5E -> 00:1a:2b:3c:4d:5e
44-
"""
45-
# Return empty string or non-string input as is
46-
if not mac or not isinstance(mac, str):
47-
return mac
48-
49-
# Check if it's an IP address (IPv4) - preserve unchanged
50-
if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', mac):
51-
return mac
52-
53-
# Try to extract MAC address from the string
54-
# Look for MAC address patterns, including those with additional text
55-
mac_patterns = [
56-
r'([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}', # Standard MAC with : or -
57-
r'([0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}', # Cisco format
58-
r'[0-9A-Fa-f]{12}' # No separators
59-
]
60-
61-
for pattern in mac_patterns:
62-
match = re.search(pattern, mac)
63-
if match:
64-
mac_candidate = match.group(0)
65-
try:
66-
# Try to parse as EUI to validate
67-
eui = EUI(mac_candidate)
68-
# Return sanitized MAC address in colon-separated lowercase format
69-
return ':'.join(['%02x' % x for x in eui.words]).lower()
70-
except (AddrFormatError, ValueError, TypeError):
71-
continue
72-
73-
# If no valid MAC found, return original string unchanged
74-
return mac
75-
76-
import swapper
7734
from openwisp_radius.registration import (
7835
REGISTRATION_METHOD_CHOICES,
7936
get_registration_choices,
@@ -223,6 +180,49 @@ def sanitize_mac_address(mac):
223180
OPTIONAL_SETTINGS = app_settings.OPTIONAL_REGISTRATION_FIELDS
224181

225182

183+
def sanitize_mac_address(mac):
184+
"""
185+
Sanitize a MAC address string to the colon-separated lowercase format.
186+
If the input is not a valid MAC address, return it unchanged.
187+
188+
Handles various MAC address formats:
189+
- 00:1A:2B:3C:4D:5E -> 00:1a:2b:3c:4d:5e
190+
- 00-1A-2B-3C-4D-5E -> 00:1a:2b:3c:4d:5e
191+
- 001A.2B3C.4D5E -> 00:1a:2b:3c:4d:5e
192+
- 001A2B3C4D5E -> 00:1a:2b:3c:4d:5e
193+
"""
194+
# Return empty string or non-string input as is
195+
if not mac or not isinstance(mac, str):
196+
return mac
197+
198+
# Check if it's an IP address (IPv4) - preserve unchanged
199+
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", mac):
200+
return mac
201+
202+
# Try to extract MAC address from the string
203+
# Look for MAC address patterns, including those with additional text
204+
mac_patterns = [
205+
r"([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}", # Standard MAC with : or -
206+
r"([0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}", # Cisco format
207+
r"[0-9A-Fa-f]{12}", # No separators
208+
]
209+
210+
for pattern in mac_patterns:
211+
match = re.search(pattern, mac)
212+
if match:
213+
mac_candidate = match.group(0)
214+
try:
215+
# Try to parse as EUI to validate
216+
eui = EUI(mac_candidate)
217+
# Return sanitized MAC address in colon-separated lowercase format
218+
return ":".join(["%02x" % x for x in eui.words]).lower()
219+
except (AddrFormatError, ValueError, TypeError):
220+
continue
221+
222+
# If no valid MAC found, return original string unchanged
223+
return mac
224+
225+
226226
class AutoUsernameMixin(object):
227227
def clean(self):
228228
"""
@@ -624,8 +624,9 @@ def _close_stale_sessions_on_nas_boot(cls, called_station_id):
624624
"""
625625
if not called_station_id:
626626
return 0
627+
sanitized_called_station_id = sanitize_mac_address(called_station_id)
627628
stale_sessions = cls.objects.filter(
628-
called_station_id=called_station_id,
629+
called_station_id=sanitized_called_station_id,
629630
stop_time__isnull=True,
630631
)
631632
closed_count = stale_sessions.update(

0 commit comments

Comments
 (0)