Skip to content

Commit 552817f

Browse files
[feature] Base logic for WHOIS lookup feature #1032 #1033 #1037 #1045
Closes #1032 Closes #1033 Closes #1037 Closes #1045 Signed-off-by: DragnEmperor <[email protected]> Co-authored-by: Federico Capoano <[email protected]>
1 parent 3a45fd9 commit 552817f

File tree

24 files changed

+1376
-11
lines changed

24 files changed

+1376
-11
lines changed

docs/developer/extending.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ Once you have created the models, add the following to your
344344
CONFIG_VPNCLIENT_MODEL = "sample_config.VpnClient"
345345
CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = "sample_config.OrganizationConfigSettings"
346346
CONFIG_ORGANIZATIONLIMITS_MODEL = "sample_config.OrganizationLimits"
347+
CONFIG_WHOISINFO_MODEL = "sample_config.WHOISInfo"
347348
DJANGO_X509_CA_MODEL = "sample_pki.Ca"
348349
DJANGO_X509_CERT_MODEL = "sample_pki.Cert"
349350
GEO_LOCATION_MODEL = "sample_geo.Location"

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ the OpenWISP architecture.
4848
user/zerotier.rst
4949
user/openvpn.rst
5050
user/subnet-division-rules.rst
51+
user/whois.rst
5152
user/rest-api.rst
5253
user/settings.rst
5354

docs/user/settings.rst

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,3 +744,54 @@ recoverable failures, improving the reliability of the system.
744744
For more information on these settings, you can refer to the `the celery
745745
documentation regarding automatic retries for known errors.
746746
<https://docs.celeryq.dev/en/stable/userguide/tasks.html#automatic-retry-for-known-exceptions>`_
747+
748+
.. _openwisp_controller_whois_enabled:
749+
750+
``OPENWISP_CONTROLLER_WHOIS_ENABLED``
751+
-------------------------------------
752+
753+
============ =========
754+
**type**: ``bool``
755+
**default**: ``False``
756+
============ =========
757+
758+
Allows enabling the optional :doc:`WHOIS Lookup feature <whois>`.
759+
760+
.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.2/whois-admin-setting.png
761+
:target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.2/whois-admin-setting.png
762+
:alt: WHOIS admin setting
763+
764+
After enabling this feature, you have to set
765+
:ref:`OPENWISP_CONTROLLER_WHOIS_GEOIP_ACCOUNT
766+
<OPENWISP_CONTROLLER_WHOIS_GEOIP_ACCOUNT>` and
767+
:ref:`OPENWISP_CONTROLLER_WHOIS_GEOIP_KEY
768+
<OPENWISP_CONTROLLER_WHOIS_GEOIP_KEY>`.
769+
770+
.. warning::
771+
772+
If these three settings are not configured as expected, an
773+
``ImproperlyConfigured`` exception will be raised.
774+
775+
.. _openwisp_controller_whois_geoip_account:
776+
777+
``OPENWISP_CONTROLLER_WHOIS_GEOIP_ACCOUNT``
778+
-------------------------------------------
779+
780+
============ =======
781+
**type**: ``str``
782+
**default**: None
783+
============ =======
784+
785+
Maxmind Account ID required for the :doc:`WHOIS Lookup feature <whois>`.
786+
787+
.. _openwisp_controller_whois_geoip_key:
788+
789+
``OPENWISP_CONTROLLER_WHOIS_GEOIP_KEY``
790+
---------------------------------------
791+
792+
============ =======
793+
**type**: ``str``
794+
**default**: None
795+
============ =======
796+
797+
Maxmind License Key required for the :doc:`WHOIS Lookup feature <whois>`.

docs/user/whois.rst

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
WHOIS Lookup
2+
============
3+
4+
.. important::
5+
6+
The **WHOIS Lookup** feature is **disabled by default**.
7+
8+
To enable it, follow the `setup steps
9+
<controller_setup_whois_lookup_>`_ below.
10+
11+
.. contents:: **Table of contents**:
12+
:depth: 1
13+
:local:
14+
15+
Overview
16+
--------
17+
18+
The WHOIS Lookup feature displays information about the public IP address
19+
used by devices to communicate with OpenWISP (via the ``last_ip`` field).
20+
It helps identify the geographic location and ISP associated with the IP
21+
address, which can be useful for troubleshooting network issues.
22+
23+
The retrieved information pertains to the Autonomous System (ASN)
24+
associated with the device's public IP address and includes:
25+
26+
- ASN (Autonomous System Number)
27+
- Organization name that owns the ASN
28+
- CIDR block assigned to the ASN
29+
- Physical address registered to the ASN
30+
- Timezone of the ASN's registered location
31+
32+
Trigger Conditions
33+
------------------
34+
35+
A WHOIS lookup is triggered automatically when:
36+
37+
- A new device is registered.
38+
- A device fetches its checksum.
39+
40+
However, the lookup will only run if **all** the following conditions are
41+
met:
42+
43+
- The device's last IP address is **public**.
44+
- There is **no existing WHOIS record** for that IP.
45+
- WHOIS lookup is **enabled** for the device's organization.
46+
47+
Behavior with Shared IP Addresses
48+
---------------------------------
49+
50+
If multiple devices share the same public IP address and one of them
51+
switches to a different IP, the following occurs:
52+
53+
- A lookup is triggered for the **new IP**.
54+
- The WHOIS record for the **old IP** is deleted.
55+
- The next time a device still using the old IP fetches its checksum, a
56+
new lookup is triggered, ensuring up-to-date data.
57+
58+
.. note::
59+
60+
When a device with an associated WHOIS record is deleted, its WHOIS
61+
record is automatically removed.
62+
63+
.. _controller_setup_whois_lookup:
64+
65+
Setup Instructions
66+
------------------
67+
68+
1. Create a MaxMind account: `Sign up here
69+
<https://www.maxmind.com/en/geolite2/signup>`_.
70+
71+
If you already have an account, just **Sign In**.
72+
73+
2. Go to **Manage License Keys** in your MaxMind dashboard.
74+
3. Generate a new license key and name it as you prefer.
75+
4. Copy both the **Account ID** and **License Key**.
76+
5. Set the following settings accordingly:
77+
78+
- Set :ref:`OPENWISP_CONTROLLER_WHOIS_ENABLED` to ``True``.
79+
- Set :ref:`OPENWISP_CONTROLLER_WHOIS_GEOIP_ACCOUNT` to **Account ID**.
80+
- Set :ref:`OPENWISP_CONTROLLER_WHOIS_GEOIP_KEY` to **License Key**.

openwisp_controller/config/admin.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,18 +1353,29 @@ def has_delete_permission(self, request, obj):
13531353

13541354

13551355
limits_inline_position = 0
1356-
if getattr(app_settings, "REGISTRATION_ENABLED", True):
13571356

1358-
class ConfigSettingsForm(AlwaysHasChangedMixin, forms.ModelForm):
1359-
class Meta:
1360-
widgets = {"context": FlatJsonWidget}
13611357

1362-
class ConfigSettingsInline(admin.StackedInline):
1363-
model = OrganizationConfigSettings
1364-
form = ConfigSettingsForm
1358+
class ConfigSettingsForm(AlwaysHasChangedMixin, forms.ModelForm):
1359+
class Meta:
1360+
widgets = {"context": FlatJsonWidget}
1361+
1362+
1363+
class ConfigSettingsInline(admin.StackedInline):
1364+
model = OrganizationConfigSettings
1365+
form = ConfigSettingsForm
1366+
1367+
def get_fields(self, request, obj=None):
1368+
fields = []
1369+
if app_settings.REGISTRATION_ENABLED:
1370+
fields += ["registration_enabled", "shared_secret"]
1371+
if app_settings.WHOIS_CONFIGURED:
1372+
fields += ["whois_enabled"]
1373+
fields += ["context"]
1374+
return fields
1375+
13651376

1366-
OrganizationAdmin.save_on_top = True
1367-
OrganizationAdmin.inlines.insert(0, ConfigSettingsInline)
1368-
limits_inline_position = 1
1377+
OrganizationAdmin.save_on_top = True
1378+
OrganizationAdmin.inlines.insert(0, ConfigSettingsInline)
1379+
limits_inline_position = 1
13691380

13701381
OrganizationAdmin.inlines.insert(limits_inline_position, OrganizationLimitsInline)

openwisp_controller/config/apps.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
vpn_peers_changed,
3232
vpn_server_modified,
3333
)
34+
from .whois.handlers import connect_whois_handlers
3435

3536
# ensure Device.hardware_id field is not flagged as unique
3637
# (because it's flagged as unique_together with organization)
@@ -52,6 +53,7 @@ def ready(self, *args, **kwargs):
5253
self.register_dashboard_charts()
5354
self.register_menu_groups()
5455
self.notification_cache_update()
56+
connect_whois_handlers()
5557

5658
def __setmodels__(self):
5759
self.device_model = load_model("config", "Device")

openwisp_controller/config/base/device.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import cached_property
12
from hashlib import md5
23

34
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
@@ -18,6 +19,7 @@
1819
management_ip_changed,
1920
)
2021
from ..validators import device_name_validator, mac_address_validator
22+
from ..whois.service import WHOISService
2123
from .base import BaseModel
2224

2325

@@ -118,6 +120,11 @@ class Meta:
118120

119121
def __init__(self, *args, **kwargs):
120122
super().__init__(*args, **kwargs)
123+
# Initial value for last_ip is required in WHOIS
124+
# to remove WHOIS info related to that ip address.
125+
if app_settings.WHOIS_CONFIGURED:
126+
self._changed_checked_fields.append("last_ip")
127+
121128
self._set_initial_values_for_changed_checked_fields()
122129

123130
def _set_initial_values_for_changed_checked_fields(self):
@@ -279,6 +286,8 @@ def save(self, *args, **kwargs):
279286
self.key = self.generate_key(shared_secret)
280287
state_adding = self._state.adding
281288
super().save(*args, **kwargs)
289+
if app_settings.WHOIS_CONFIGURED:
290+
self._check_last_ip()
282291
if state_adding and self.group and self.group.templates.exists():
283292
self.create_default_config()
284293
# The value of "self._state.adding" will always be "False"
@@ -299,7 +308,9 @@ def _check_changed_fields(self):
299308
self._get_initial_values_for_checked_fields()
300309
# Execute method for checked for each field in self._changed_checked_fields
301310
for field in self._changed_checked_fields:
302-
getattr(self, f"_check_{field}_changed")()
311+
method = getattr(self, f"_check_{field}_changed", None)
312+
if callable(method):
313+
method()
303314

304315
def _is_deferred(self, field):
305316
"""
@@ -490,3 +501,18 @@ def config_deactivated_clear_management_ip(cls, instance, *args, **kwargs):
490501
is changed to 'deactivated'.
491502
"""
492503
cls.objects.filter(pk=instance.device_id).update(management_ip="")
504+
505+
@cached_property
506+
def whois_service(self):
507+
"""
508+
Used as a shortcut to get WHOISService instance
509+
for the device.
510+
"""
511+
return WHOISService(self)
512+
513+
def _check_last_ip(self):
514+
"""Trigger WHOIS lookup if last_ip is not deferred."""
515+
if self._initial_last_ip == models.DEFERRED:
516+
return
517+
self.whois_service.trigger_whois_lookup()
518+
self._initial_last_ip = self.last_ip

openwisp_controller/config/base/multitenancy.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
from copy import deepcopy
33

44
import swapper
5+
from django.core.exceptions import ValidationError
56
from django.db import models
67
from django.utils.translation import gettext_lazy as _
78
from jsonfield import JSONField
89

910
from openwisp_utils.base import KeyField, UUIDModel
11+
from openwisp_utils.fields import FallbackBooleanChoiceField
1012

13+
from .. import settings as app_settings
1114
from ..exceptions import OrganizationDeviceLimitExceeded
1215
from ..tasks import bulk_invalidate_config_get_cached_checksum
1316

@@ -31,6 +34,11 @@ class AbstractOrganizationConfigSettings(UUIDModel):
3134
verbose_name=_("shared secret"),
3235
help_text=_("used for automatic registration of devices"),
3336
)
37+
whois_enabled = FallbackBooleanChoiceField(
38+
help_text=_("Whether the WHOIS lookup feature is enabled"),
39+
fallback=app_settings.WHOIS_ENABLED,
40+
verbose_name=_("WHOIS Enabled"),
41+
)
3442
context = JSONField(
3543
blank=True,
3644
default=dict,
@@ -53,6 +61,18 @@ def __str__(self):
5361
def get_context(self):
5462
return deepcopy(self.context)
5563

64+
def clean(self):
65+
if not app_settings.WHOIS_CONFIGURED and self.whois_enabled:
66+
raise ValidationError(
67+
{
68+
"whois_enabled": _(
69+
"WHOIS_GEOIP_ACCOUNT and WHOIS_GEOIP_KEY must be set "
70+
+ "before enabling WHOIS feature."
71+
)
72+
}
73+
)
74+
return super().clean()
75+
5676
def save(
5777
self, force_insert=False, force_update=False, using=None, update_fields=None
5878
):

0 commit comments

Comments
 (0)