Autarch Will Control The Internet
This commit is contained in:
349
services/dns-server/server/hosts.go
Normal file
349
services/dns-server/server/hosts.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// HostEntry represents a single hosts file entry.
|
||||
type HostEntry struct {
|
||||
IP string `json:"ip"`
|
||||
Hostname string `json:"hostname"`
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// HostsStore manages a hosts-file-like database.
|
||||
type HostsStore struct {
|
||||
mu sync.RWMutex
|
||||
entries []HostEntry
|
||||
path string // path to hosts file on disk (if loaded from file)
|
||||
}
|
||||
|
||||
// NewHostsStore creates a new hosts store.
|
||||
func NewHostsStore() *HostsStore {
|
||||
return &HostsStore{
|
||||
entries: make([]HostEntry, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadFile parses a hosts file from disk.
|
||||
func (h *HostsStore) LoadFile(path string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
h.path = path
|
||||
h.entries = h.entries[:0]
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Strip inline comments
|
||||
comment := ""
|
||||
if idx := strings.Index(line, "#"); idx >= 0 {
|
||||
comment = strings.TrimSpace(line[idx+1:])
|
||||
line = strings.TrimSpace(line[:idx])
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
ip := fields[0]
|
||||
if net.ParseIP(ip) == nil {
|
||||
continue // invalid IP
|
||||
}
|
||||
|
||||
entry := HostEntry{
|
||||
IP: ip,
|
||||
Hostname: strings.ToLower(fields[1]),
|
||||
Comment: comment,
|
||||
}
|
||||
if len(fields) > 2 {
|
||||
aliases := make([]string, len(fields)-2)
|
||||
for i, a := range fields[2:] {
|
||||
aliases[i] = strings.ToLower(a)
|
||||
}
|
||||
entry.Aliases = aliases
|
||||
}
|
||||
h.entries = append(h.entries, entry)
|
||||
}
|
||||
|
||||
log.Printf("[hosts] Loaded %d entries from %s", len(h.entries), path)
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// LoadFromText parses hosts-format text (like pasting /etc/hosts content).
|
||||
func (h *HostsStore) LoadFromText(content string) int {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
count := 0
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
comment := ""
|
||||
if idx := strings.Index(line, "#"); idx >= 0 {
|
||||
comment = strings.TrimSpace(line[idx+1:])
|
||||
line = strings.TrimSpace(line[:idx])
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
ip := fields[0]
|
||||
if net.ParseIP(ip) == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
entry := HostEntry{
|
||||
IP: ip,
|
||||
Hostname: strings.ToLower(fields[1]),
|
||||
Comment: comment,
|
||||
}
|
||||
if len(fields) > 2 {
|
||||
aliases := make([]string, len(fields)-2)
|
||||
for i, a := range fields[2:] {
|
||||
aliases[i] = strings.ToLower(a)
|
||||
}
|
||||
entry.Aliases = aliases
|
||||
}
|
||||
|
||||
// Dedup by hostname
|
||||
found := false
|
||||
for i, e := range h.entries {
|
||||
if e.Hostname == entry.Hostname {
|
||||
h.entries[i] = entry
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
h.entries = append(h.entries, entry)
|
||||
}
|
||||
count++
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
// Add adds a single host entry.
|
||||
func (h *HostsStore) Add(ip, hostname string, aliases []string, comment string) error {
|
||||
if net.ParseIP(ip) == nil {
|
||||
return fmt.Errorf("invalid IP: %s", ip)
|
||||
}
|
||||
hostname = strings.ToLower(strings.TrimSpace(hostname))
|
||||
if hostname == "" {
|
||||
return fmt.Errorf("hostname required")
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// Check for duplicate
|
||||
for i, e := range h.entries {
|
||||
if e.Hostname == hostname {
|
||||
h.entries[i].IP = ip
|
||||
h.entries[i].Aliases = aliases
|
||||
h.entries[i].Comment = comment
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
h.entries = append(h.entries, HostEntry{
|
||||
IP: ip,
|
||||
Hostname: hostname,
|
||||
Aliases: aliases,
|
||||
Comment: comment,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a host entry by hostname.
|
||||
func (h *HostsStore) Remove(hostname string) bool {
|
||||
hostname = strings.ToLower(strings.TrimSpace(hostname))
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
for i, e := range h.entries {
|
||||
if e.Hostname == hostname {
|
||||
h.entries = append(h.entries[:i], h.entries[i+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Clear removes all entries.
|
||||
func (h *HostsStore) Clear() int {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
n := len(h.entries)
|
||||
h.entries = h.entries[:0]
|
||||
return n
|
||||
}
|
||||
|
||||
// List returns all entries.
|
||||
func (h *HostsStore) List() []HostEntry {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
result := make([]HostEntry, len(h.entries))
|
||||
copy(result, h.entries)
|
||||
return result
|
||||
}
|
||||
|
||||
// Count returns the number of entries.
|
||||
func (h *HostsStore) Count() int {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return len(h.entries)
|
||||
}
|
||||
|
||||
// Lookup resolves a hostname from the hosts store.
|
||||
// Returns DNS RRs matching the query name and type.
|
||||
func (h *HostsStore) Lookup(name string, qtype uint16) []dns.RR {
|
||||
if qtype != dns.TypeA && qtype != dns.TypeAAAA && qtype != dns.TypePTR {
|
||||
return nil
|
||||
}
|
||||
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
fqdn := dns.Fqdn(strings.ToLower(name))
|
||||
baseName := strings.TrimSuffix(fqdn, ".")
|
||||
|
||||
// PTR lookup (reverse DNS)
|
||||
if qtype == dns.TypePTR {
|
||||
// Convert in-addr.arpa name to IP
|
||||
ip := ptrToIP(fqdn)
|
||||
if ip == "" {
|
||||
return nil
|
||||
}
|
||||
for _, e := range h.entries {
|
||||
if e.IP == ip {
|
||||
rr := &dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: fqdn,
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 60,
|
||||
},
|
||||
Ptr: dns.Fqdn(e.Hostname),
|
||||
}
|
||||
return []dns.RR{rr}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Forward lookup (A / AAAA)
|
||||
var results []dns.RR
|
||||
for _, e := range h.entries {
|
||||
// Match hostname or aliases
|
||||
match := strings.EqualFold(e.Hostname, baseName) || strings.EqualFold(dns.Fqdn(e.Hostname), fqdn)
|
||||
if !match {
|
||||
for _, a := range e.Aliases {
|
||||
if strings.EqualFold(a, baseName) || strings.EqualFold(dns.Fqdn(a), fqdn) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
ip := net.ParseIP(e.IP)
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if qtype == dns.TypeA && ip.To4() != nil {
|
||||
rr := &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: fqdn,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 60,
|
||||
},
|
||||
A: ip.To4(),
|
||||
}
|
||||
results = append(results, rr)
|
||||
} else if qtype == dns.TypeAAAA && ip.To4() == nil {
|
||||
rr := &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: fqdn,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 60,
|
||||
},
|
||||
AAAA: ip,
|
||||
}
|
||||
results = append(results, rr)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Export returns hosts file format text.
|
||||
func (h *HostsStore) Export() string {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("# AUTARCH DNS hosts file\n")
|
||||
sb.WriteString(fmt.Sprintf("# Generated: %s\n", time.Now().UTC().Format(time.RFC3339)))
|
||||
sb.WriteString("# Entries: " + fmt.Sprintf("%d", len(h.entries)) + "\n\n")
|
||||
|
||||
for _, e := range h.entries {
|
||||
line := e.IP + "\t" + e.Hostname
|
||||
for _, a := range e.Aliases {
|
||||
line += "\t" + a
|
||||
}
|
||||
if e.Comment != "" {
|
||||
line += "\t# " + e.Comment
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ptrToIP converts a PTR domain name (in-addr.arpa) to an IP string.
|
||||
func ptrToIP(name string) string {
|
||||
name = strings.TrimSuffix(strings.ToLower(name), ".")
|
||||
if !strings.HasSuffix(name, ".in-addr.arpa") {
|
||||
return ""
|
||||
}
|
||||
name = strings.TrimSuffix(name, ".in-addr.arpa")
|
||||
parts := strings.Split(name, ".")
|
||||
if len(parts) != 4 {
|
||||
return ""
|
||||
}
|
||||
// Reverse the octets
|
||||
return parts[3] + "." + parts[2] + "." + parts[1] + "." + parts[0]
|
||||
}
|
||||
Reference in New Issue
Block a user