DigiJ 2322f69516 v2.2.0 — Full arsenal expansion: 16 new security modules
Add WiFi Audit, API Fuzzer, Cloud Scanner, Threat Intel, Log Correlator,
Steganography, Anti-Forensics, BLE Scanner, Forensics, RFID/NFC, Malware
Sandbox, Password Toolkit, Web Scanner, Report Engine, Net Mapper, and
C2 Framework. Each module includes CLI interface, Flask routes, and web
UI template. Also includes Go DNS server source + binary, IP Capture
service, SYN Flood, Gone Fishing mail server, and hack hijack modules
from v2.0 work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 05:20:39 -08:00

526 lines
13 KiB
Go

package server
import (
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/miekg/dns"
)
// RecordType represents supported DNS record types.
type RecordType string
const (
TypeA RecordType = "A"
TypeAAAA RecordType = "AAAA"
TypeCNAME RecordType = "CNAME"
TypeMX RecordType = "MX"
TypeTXT RecordType = "TXT"
TypeNS RecordType = "NS"
TypeSRV RecordType = "SRV"
TypePTR RecordType = "PTR"
TypeSOA RecordType = "SOA"
)
// Record is a single DNS record.
type Record struct {
ID string `json:"id"`
Type RecordType `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
TTL uint32 `json:"ttl"`
Priority uint16 `json:"priority,omitempty"` // MX, SRV
Weight uint16 `json:"weight,omitempty"` // SRV
Port uint16 `json:"port,omitempty"` // SRV
}
// Zone represents a DNS zone with its records.
type Zone struct {
Domain string `json:"domain"`
SOA SOARecord `json:"soa"`
Records []Record `json:"records"`
DNSSEC bool `json:"dnssec"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// SOARecord holds SOA-specific fields.
type SOARecord struct {
PrimaryNS string `json:"primary_ns"`
AdminEmail string `json:"admin_email"`
Serial uint32 `json:"serial"`
Refresh uint32 `json:"refresh"`
Retry uint32 `json:"retry"`
Expire uint32 `json:"expire"`
MinTTL uint32 `json:"min_ttl"`
}
// ZoneStore manages zones on disk and in memory.
type ZoneStore struct {
mu sync.RWMutex
zones map[string]*Zone
zonesDir string
}
// NewZoneStore creates a store backed by a directory.
func NewZoneStore(dir string) *ZoneStore {
os.MkdirAll(dir, 0755)
return &ZoneStore{
zones: make(map[string]*Zone),
zonesDir: dir,
}
}
// LoadAll reads all zone files from disk.
func (s *ZoneStore) LoadAll() error {
entries, err := os.ReadDir(s.zonesDir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
for _, e := range entries {
if filepath.Ext(e.Name()) != ".json" {
continue
}
data, err := os.ReadFile(filepath.Join(s.zonesDir, e.Name()))
if err != nil {
continue
}
var z Zone
if err := json.Unmarshal(data, &z); err != nil {
continue
}
s.zones[dns.Fqdn(z.Domain)] = &z
}
return nil
}
// Save writes a zone to disk.
func (s *ZoneStore) Save(z *Zone) error {
z.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
data, err := json.MarshalIndent(z, "", " ")
if err != nil {
return err
}
fname := filepath.Join(s.zonesDir, z.Domain+".json")
return os.WriteFile(fname, data, 0644)
}
// Get returns a zone by domain.
func (s *ZoneStore) Get(domain string) *Zone {
s.mu.RLock()
defer s.mu.RUnlock()
return s.zones[dns.Fqdn(domain)]
}
// List returns all zones.
func (s *ZoneStore) List() []*Zone {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]*Zone, 0, len(s.zones))
for _, z := range s.zones {
result = append(result, z)
}
return result
}
// Create adds a new zone.
func (s *ZoneStore) Create(domain string) (*Zone, error) {
fqdn := dns.Fqdn(domain)
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.zones[fqdn]; exists {
return nil, fmt.Errorf("zone %s already exists", domain)
}
now := time.Now().UTC().Format(time.RFC3339)
z := &Zone{
Domain: domain,
SOA: SOARecord{
PrimaryNS: "ns1." + domain,
AdminEmail: "admin." + domain,
Serial: uint32(time.Now().Unix()),
Refresh: 3600,
Retry: 600,
Expire: 86400,
MinTTL: 300,
},
Records: []Record{
{ID: "ns1", Type: TypeNS, Name: domain + ".", Value: "ns1." + domain + ".", TTL: 3600},
},
CreatedAt: now,
UpdatedAt: now,
}
s.zones[fqdn] = z
return z, s.Save(z)
}
// Delete removes a zone.
func (s *ZoneStore) Delete(domain string) error {
fqdn := dns.Fqdn(domain)
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.zones[fqdn]; !exists {
return fmt.Errorf("zone %s not found", domain)
}
delete(s.zones, fqdn)
fname := filepath.Join(s.zonesDir, domain+".json")
os.Remove(fname)
return nil
}
// AddRecord adds a record to a zone.
func (s *ZoneStore) AddRecord(domain string, rec Record) error {
fqdn := dns.Fqdn(domain)
s.mu.Lock()
defer s.mu.Unlock()
z, ok := s.zones[fqdn]
if !ok {
return fmt.Errorf("zone %s not found", domain)
}
if rec.ID == "" {
rec.ID = fmt.Sprintf("r%d", time.Now().UnixNano())
}
if rec.TTL == 0 {
rec.TTL = 300
}
z.Records = append(z.Records, rec)
z.SOA.Serial++
return s.Save(z)
}
// DeleteRecord removes a record by ID.
func (s *ZoneStore) DeleteRecord(domain, recordID string) error {
fqdn := dns.Fqdn(domain)
s.mu.Lock()
defer s.mu.Unlock()
z, ok := s.zones[fqdn]
if !ok {
return fmt.Errorf("zone %s not found", domain)
}
for i, r := range z.Records {
if r.ID == recordID {
z.Records = append(z.Records[:i], z.Records[i+1:]...)
z.SOA.Serial++
return s.Save(z)
}
}
return fmt.Errorf("record %s not found", recordID)
}
// UpdateRecord updates a record by ID.
func (s *ZoneStore) UpdateRecord(domain, recordID string, rec Record) error {
fqdn := dns.Fqdn(domain)
s.mu.Lock()
defer s.mu.Unlock()
z, ok := s.zones[fqdn]
if !ok {
return fmt.Errorf("zone %s not found", domain)
}
for i, r := range z.Records {
if r.ID == recordID {
rec.ID = recordID
z.Records[i] = rec
z.SOA.Serial++
return s.Save(z)
}
}
return fmt.Errorf("record %s not found", recordID)
}
// Lookup finds records matching a query name and type within all zones.
func (s *ZoneStore) Lookup(name string, qtype uint16) []dns.RR {
s.mu.RLock()
defer s.mu.RUnlock()
fqdn := dns.Fqdn(name)
var results []dns.RR
// Find the zone for this name
for zoneDomain, z := range s.zones {
if !dns.IsSubDomain(zoneDomain, fqdn) {
continue
}
// Check records
for _, rec := range z.Records {
recFQDN := dns.Fqdn(rec.Name)
if recFQDN != fqdn {
continue
}
if rr := recordToRR(rec, fqdn); rr != nil {
if qtype == dns.TypeANY || rr.Header().Rrtype == qtype {
results = append(results, rr)
}
}
}
// SOA for zone apex
if fqdn == zoneDomain && (qtype == dns.TypeSOA || qtype == dns.TypeANY) {
soa := &dns.SOA{
Hdr: dns.RR_Header{Name: zoneDomain, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: z.SOA.MinTTL},
Ns: dns.Fqdn(z.SOA.PrimaryNS),
Mbox: dns.Fqdn(z.SOA.AdminEmail),
Serial: z.SOA.Serial,
Refresh: z.SOA.Refresh,
Retry: z.SOA.Retry,
Expire: z.SOA.Expire,
Minttl: z.SOA.MinTTL,
}
results = append(results, soa)
}
}
return results
}
func recordToRR(rec Record, fqdn string) dns.RR {
hdr := dns.RR_Header{Name: fqdn, Class: dns.ClassINET, Ttl: rec.TTL}
switch rec.Type {
case TypeA:
hdr.Rrtype = dns.TypeA
rr := &dns.A{Hdr: hdr}
rr.A = parseIP(rec.Value)
if rr.A == nil {
return nil
}
return rr
case TypeAAAA:
hdr.Rrtype = dns.TypeAAAA
rr := &dns.AAAA{Hdr: hdr}
rr.AAAA = parseIP(rec.Value)
if rr.AAAA == nil {
return nil
}
return rr
case TypeCNAME:
hdr.Rrtype = dns.TypeCNAME
return &dns.CNAME{Hdr: hdr, Target: dns.Fqdn(rec.Value)}
case TypeMX:
hdr.Rrtype = dns.TypeMX
return &dns.MX{Hdr: hdr, Preference: rec.Priority, Mx: dns.Fqdn(rec.Value)}
case TypeTXT:
hdr.Rrtype = dns.TypeTXT
return &dns.TXT{Hdr: hdr, Txt: []string{rec.Value}}
case TypeNS:
hdr.Rrtype = dns.TypeNS
return &dns.NS{Hdr: hdr, Ns: dns.Fqdn(rec.Value)}
case TypeSRV:
hdr.Rrtype = dns.TypeSRV
return &dns.SRV{Hdr: hdr, Priority: rec.Priority, Weight: rec.Weight, Port: rec.Port, Target: dns.Fqdn(rec.Value)}
case TypePTR:
hdr.Rrtype = dns.TypePTR
return &dns.PTR{Hdr: hdr, Ptr: dns.Fqdn(rec.Value)}
}
return nil
}
func parseIP(s string) net.IP {
return net.ParseIP(s)
}
// ExportZoneFile exports a zone in BIND zone file format.
func (s *ZoneStore) ExportZoneFile(domain string) (string, error) {
s.mu.RLock()
defer s.mu.RUnlock()
z, ok := s.zones[dns.Fqdn(domain)]
if !ok {
return "", fmt.Errorf("zone %s not found", domain)
}
var b strings.Builder
b.WriteString(fmt.Sprintf("; Zone file for %s\n", z.Domain))
b.WriteString(fmt.Sprintf("; Exported at %s\n", time.Now().UTC().Format(time.RFC3339)))
b.WriteString(fmt.Sprintf("$ORIGIN %s.\n", z.Domain))
b.WriteString(fmt.Sprintf("$TTL %d\n\n", z.SOA.MinTTL))
// SOA
b.WriteString(fmt.Sprintf("@ IN SOA %s. %s. (\n", z.SOA.PrimaryNS, z.SOA.AdminEmail))
b.WriteString(fmt.Sprintf(" %d ; serial\n", z.SOA.Serial))
b.WriteString(fmt.Sprintf(" %d ; refresh\n", z.SOA.Refresh))
b.WriteString(fmt.Sprintf(" %d ; retry\n", z.SOA.Retry))
b.WriteString(fmt.Sprintf(" %d ; expire\n", z.SOA.Expire))
b.WriteString(fmt.Sprintf(" %d ; minimum TTL\n)\n\n", z.SOA.MinTTL))
// Records grouped by type
for _, rec := range z.Records {
name := rec.Name
// Make relative to origin
suffix := "." + z.Domain + "."
if strings.HasSuffix(name, suffix) {
name = strings.TrimSuffix(name, suffix)
} else if name == z.Domain+"." {
name = "@"
}
switch rec.Type {
case TypeMX:
b.WriteString(fmt.Sprintf("%-24s %d IN MX %d %s\n", name, rec.TTL, rec.Priority, rec.Value))
case TypeSRV:
b.WriteString(fmt.Sprintf("%-24s %d IN SRV %d %d %d %s\n", name, rec.TTL, rec.Priority, rec.Weight, rec.Port, rec.Value))
default:
b.WriteString(fmt.Sprintf("%-24s %d IN %-6s %s\n", name, rec.TTL, rec.Type, rec.Value))
}
}
return b.String(), nil
}
// ImportZoneFile parses a BIND-style zone file and adds records.
// Returns number of records added.
func (s *ZoneStore) ImportZoneFile(domain, content string) (int, error) {
fqdn := dns.Fqdn(domain)
s.mu.Lock()
defer s.mu.Unlock()
z, ok := s.zones[fqdn]
if !ok {
return 0, fmt.Errorf("zone %s not found — create it first", domain)
}
added := 0
zp := dns.NewZoneParser(strings.NewReader(content), dns.Fqdn(domain), "")
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
hdr := rr.Header()
rec := Record{
ID: fmt.Sprintf("imp%d", time.Now().UnixNano()+int64(added)),
Name: hdr.Name,
TTL: hdr.Ttl,
}
switch v := rr.(type) {
case *dns.A:
rec.Type = TypeA
rec.Value = v.A.String()
case *dns.AAAA:
rec.Type = TypeAAAA
rec.Value = v.AAAA.String()
case *dns.CNAME:
rec.Type = TypeCNAME
rec.Value = v.Target
case *dns.MX:
rec.Type = TypeMX
rec.Value = v.Mx
rec.Priority = v.Preference
case *dns.TXT:
rec.Type = TypeTXT
rec.Value = strings.Join(v.Txt, " ")
case *dns.NS:
rec.Type = TypeNS
rec.Value = v.Ns
case *dns.SRV:
rec.Type = TypeSRV
rec.Value = v.Target
rec.Priority = v.Priority
rec.Weight = v.Weight
rec.Port = v.Port
case *dns.PTR:
rec.Type = TypePTR
rec.Value = v.Ptr
default:
continue // Skip unsupported types
}
z.Records = append(z.Records, rec)
added++
}
if added > 0 {
z.SOA.Serial++
s.Save(z)
}
return added, nil
}
// CloneZone duplicates a zone under a new domain.
func (s *ZoneStore) CloneZone(srcDomain, dstDomain string) (*Zone, error) {
srcFQDN := dns.Fqdn(srcDomain)
dstFQDN := dns.Fqdn(dstDomain)
s.mu.Lock()
defer s.mu.Unlock()
src, ok := s.zones[srcFQDN]
if !ok {
return nil, fmt.Errorf("source zone %s not found", srcDomain)
}
if _, exists := s.zones[dstFQDN]; exists {
return nil, fmt.Errorf("destination zone %s already exists", dstDomain)
}
now := time.Now().UTC().Format(time.RFC3339)
z := &Zone{
Domain: dstDomain,
SOA: SOARecord{
PrimaryNS: strings.Replace(src.SOA.PrimaryNS, srcDomain, dstDomain, -1),
AdminEmail: strings.Replace(src.SOA.AdminEmail, srcDomain, dstDomain, -1),
Serial: uint32(time.Now().Unix()),
Refresh: src.SOA.Refresh,
Retry: src.SOA.Retry,
Expire: src.SOA.Expire,
MinTTL: src.SOA.MinTTL,
},
CreatedAt: now,
UpdatedAt: now,
}
// Clone records, replacing domain references
for _, rec := range src.Records {
newRec := rec
newRec.ID = fmt.Sprintf("c%d", time.Now().UnixNano())
newRec.Name = strings.Replace(rec.Name, srcDomain, dstDomain, -1)
newRec.Value = strings.Replace(rec.Value, srcDomain, dstDomain, -1)
z.Records = append(z.Records, newRec)
time.Sleep(time.Nanosecond) // Ensure unique IDs
}
s.zones[dstFQDN] = z
return z, s.Save(z)
}
// BulkAddRecords adds multiple records at once.
func (s *ZoneStore) BulkAddRecords(domain string, records []Record) (int, error) {
fqdn := dns.Fqdn(domain)
s.mu.Lock()
defer s.mu.Unlock()
z, ok := s.zones[fqdn]
if !ok {
return 0, fmt.Errorf("zone %s not found", domain)
}
added := 0
for _, rec := range records {
if rec.ID == "" {
rec.ID = fmt.Sprintf("b%d", time.Now().UnixNano()+int64(added))
}
if rec.TTL == 0 {
rec.TTL = 300
}
z.Records = append(z.Records, rec)
added++
}
if added > 0 {
z.SOA.Serial++
s.Save(z)
}
return added, nil
}