from django.db import models class RouterMonSettings(models.Model): """Singleton (pk=1) — router syslog receiver configuration.""" enabled = models.BooleanField(default=False) syslog_port = models.IntegerField( default=5514, help_text=( 'UDP port to listen on for syslog datagrams. Default: 5514 (non-privileged). ' 'If your router only supports port 514, add an iptables redirect on the k3s node: ' 'iptables -t nat -A PREROUTING -p udp --dport 514 -j REDIRECT --to-port 5514' ), ) excluded_clients = models.TextField( blank=True, default='', help_text='One LAN IP per line. DNS queries from these clients will not be stored.', ) retention_days = models.IntegerField( default=7, help_text='Delete DNS query records older than this many days.', ) last_received_at = models.DateTimeField(null=True, blank=True) class Meta: verbose_name = 'Router Monitor Settings' verbose_name_plural = 'Router Monitor Settings' def __str__(self): status = 'enabled' if self.enabled else 'disabled' return f'RouterMon Settings ({status}, port {self.syslog_port})' @classmethod def get(cls): obj, _ = cls.objects.get_or_create(pk=1) return obj class DnsQuery(models.Model): """One DNS query event received from the router's dnsmasq syslog.""" timestamp = models.DateTimeField(db_index=True) client_ip = models.GenericIPAddressField(db_index=True) client_name = models.CharField(max_length=200, blank=True, help_text='Hostname from DHCP lease at query time') domain = models.CharField(max_length=500, db_index=True) query_type = models.CharField(max_length=20, default='A') is_nxdomain = models.BooleanField(default=False, db_index=True) # Best-effort: first public A/AAAA record from the corresponding reply line. resolved_ip = models.GenericIPAddressField(null=True, blank=True) # Geo for resolved_ip (populated asynchronously) country = models.CharField(max_length=100, blank=True) country_code = models.CharField(max_length=10, blank=True) city = models.CharField(max_length=100, blank=True) class Meta: ordering = ['-timestamp'] indexes = [ models.Index(fields=['client_ip', 'timestamp']), models.Index(fields=['domain', 'timestamp']), models.Index(fields=['timestamp', 'is_nxdomain']), ] def __str__(self): flag = ' [NX]' if self.is_nxdomain else '' return f'{self.client_ip} → {self.domain} ({self.query_type}){flag} @ {self.timestamp:%Y-%m-%d %H:%M:%S}' @property def geo_display(self): parts = [p for p in [self.city, self.country] if p] return ', '.join(parts) if parts else '—' class WanEvent(models.Model): """One incoming WAN connection attempt logged by iptables (WAN_IN: prefix).""" timestamp = models.DateTimeField(db_index=True) src_ip = models.GenericIPAddressField(db_index=True) dst_port = models.IntegerField(null=True, blank=True, db_index=True) protocol = models.CharField(max_length=10) # TCP / UDP / ICMP src_port = models.IntegerField(null=True, blank=True) # Geo for src_ip (populated asynchronously) country = models.CharField(max_length=100, blank=True) country_code = models.CharField(max_length=10, blank=True) city = models.CharField(max_length=100, blank=True) isp = models.CharField(max_length=200, blank=True) class Meta: ordering = ['-timestamp'] indexes = [ models.Index(fields=['src_ip', 'timestamp']), models.Index(fields=['dst_port', 'timestamp']), models.Index(fields=['timestamp', 'protocol']), ] def __str__(self): port = f':{self.dst_port}' if self.dst_port else '' return f'{self.src_ip} → {self.protocol}{port} @ {self.timestamp:%Y-%m-%d %H:%M:%S}' @property def geo_display(self): parts = [p for p in [self.city, self.country] if p] return ', '.join(parts) if parts else '—' class DhcpLease(models.Model): """Latest-known DHCP lease: maps LAN IP to MAC + hostname.""" ip = models.GenericIPAddressField(unique=True, db_index=True) mac = models.CharField(max_length=17, blank=True) hostname = models.CharField(max_length=200, blank=True) last_seen = models.DateTimeField(auto_now=True) class Meta: ordering = ['ip'] def __str__(self): return f'{self.ip} ({self.hostname or self.mac or "unknown"})'