bug fixing

This commit is contained in:
anonpenguin23 2026-01-24 17:37:52 +02:00
parent 3d3b0d2ee6
commit 6101455f4a
10 changed files with 276 additions and 51 deletions

BIN
cli

Binary file not shown.

BIN
gateway

Binary file not shown.

View File

@ -0,0 +1,45 @@
-- Migration 009: Update DNS Records to Support Multiple Records per FQDN
-- This allows round-robin A records and multiple NS records for the same domain
BEGIN;
-- SQLite doesn't support DROP CONSTRAINT, so we recreate the table
-- First, create the new table structure
CREATE TABLE IF NOT EXISTS dns_records_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
fqdn TEXT NOT NULL, -- Fully qualified domain name (e.g., myapp.node-7prvNa.orama.network)
record_type TEXT NOT NULL DEFAULT 'A',-- DNS record type: A, AAAA, CNAME, TXT, NS, SOA
value TEXT NOT NULL, -- IP address or target value
ttl INTEGER NOT NULL DEFAULT 300, -- Time to live in seconds
priority INTEGER DEFAULT 0, -- Priority for MX/SRV records, or weight for round-robin
namespace TEXT NOT NULL DEFAULT 'system', -- Namespace that owns this record
deployment_id TEXT, -- Optional: deployment that created this record
node_id TEXT, -- Optional: specific node ID for node-specific routing
is_active BOOLEAN NOT NULL DEFAULT TRUE,-- Enable/disable without deleting
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by TEXT NOT NULL DEFAULT 'system', -- Wallet address or 'system' for auto-created records
UNIQUE(fqdn, record_type, value) -- Allow multiple records of same type for same FQDN, but not duplicates
);
-- Copy existing data if the old table exists
INSERT OR IGNORE INTO dns_records_new (id, fqdn, record_type, value, ttl, namespace, deployment_id, node_id, is_active, created_at, updated_at, created_by)
SELECT id, fqdn, record_type, value, ttl, namespace, deployment_id, node_id, is_active, created_at, updated_at, created_by
FROM dns_records WHERE 1=1;
-- Drop old table and rename new one
DROP TABLE IF EXISTS dns_records;
ALTER TABLE dns_records_new RENAME TO dns_records;
-- Recreate indexes
CREATE INDEX IF NOT EXISTS idx_dns_records_fqdn ON dns_records(fqdn);
CREATE INDEX IF NOT EXISTS idx_dns_records_fqdn_type ON dns_records(fqdn, record_type);
CREATE INDEX IF NOT EXISTS idx_dns_records_namespace ON dns_records(namespace);
CREATE INDEX IF NOT EXISTS idx_dns_records_deployment ON dns_records(deployment_id);
CREATE INDEX IF NOT EXISTS idx_dns_records_node_id ON dns_records(node_id);
CREATE INDEX IF NOT EXISTS idx_dns_records_active ON dns_records(is_active);
-- Mark migration as applied
INSERT OR IGNORE INTO schema_migrations(version) VALUES (9);
COMMIT;

View File

@ -341,6 +341,18 @@ func (o *Orchestrator) restartServices() error {
// Restart services to apply changes - use getProductionServices to only restart existing services // Restart services to apply changes - use getProductionServices to only restart existing services
services := utils.GetProductionServices() services := utils.GetProductionServices()
// If this is a nameserver, also restart CoreDNS and Caddy
if o.setup.IsNameserver() {
nameserverServices := []string{"coredns", "caddy"}
for _, svc := range nameserverServices {
unitPath := filepath.Join("/etc/systemd/system", svc+".service")
if _, err := os.Stat(unitPath); err == nil {
services = append(services, svc)
}
}
}
if len(services) == 0 { if len(services) == 0 {
fmt.Printf(" ⚠️ No services found to restart\n") fmt.Printf(" ⚠️ No services found to restart\n")
} else { } else {

View File

@ -142,11 +142,66 @@ func (b *Backend) parseValue(recordType, value string) (interface{}, error) {
case "TXT": case "TXT":
return []string{value}, nil return []string{value}, nil
case "NS":
return dns.Fqdn(value), nil
case "SOA":
// SOA format: "mname rname serial refresh retry expire minimum"
// Example: "ns1.dbrs.space. admin.dbrs.space. 2026012401 3600 1800 604800 300"
return b.parseSOA(value)
default: default:
return nil, fmt.Errorf("unsupported record type: %s", recordType) return nil, fmt.Errorf("unsupported record type: %s", recordType)
} }
} }
// parseSOA parses a SOA record value string
// Format: "mname rname serial refresh retry expire minimum"
func (b *Backend) parseSOA(value string) (*dns.SOA, error) {
parts := strings.Fields(value)
if len(parts) < 7 {
return nil, fmt.Errorf("invalid SOA format, expected 7 fields: %s", value)
}
serial, err := parseUint32(parts[2])
if err != nil {
return nil, fmt.Errorf("invalid SOA serial: %w", err)
}
refresh, err := parseUint32(parts[3])
if err != nil {
return nil, fmt.Errorf("invalid SOA refresh: %w", err)
}
retry, err := parseUint32(parts[4])
if err != nil {
return nil, fmt.Errorf("invalid SOA retry: %w", err)
}
expire, err := parseUint32(parts[5])
if err != nil {
return nil, fmt.Errorf("invalid SOA expire: %w", err)
}
minttl, err := parseUint32(parts[6])
if err != nil {
return nil, fmt.Errorf("invalid SOA minimum: %w", err)
}
return &dns.SOA{
Ns: dns.Fqdn(parts[0]),
Mbox: dns.Fqdn(parts[1]),
Serial: serial,
Refresh: refresh,
Retry: retry,
Expire: expire,
Minttl: minttl,
}, nil
}
// parseUint32 parses a string to uint32
func parseUint32(s string) (uint32, error) {
var val uint32
_, err := fmt.Sscanf(s, "%d", &val)
return val, err
}
// ping tests the RQLite connection // ping tests the RQLite connection
func (b *Backend) ping() error { func (b *Backend) ping() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@ -205,6 +260,10 @@ func qTypeToString(qtype uint16) string {
return "CNAME" return "CNAME"
case dns.TypeTXT: case dns.TypeTXT:
return "TXT" return "TXT"
case dns.TypeNS:
return "NS"
case dns.TypeSOA:
return "SOA"
default: default:
return dns.TypeToString[qtype] return dns.TypeToString[qtype]
} }
@ -221,6 +280,10 @@ func stringToQType(s string) uint16 {
return dns.TypeCNAME return dns.TypeCNAME
case "TXT": case "TXT":
return dns.TypeTXT return dns.TypeTXT
case "NS":
return dns.TypeNS
case "SOA":
return dns.TypeSOA
default: default:
return 0 return 0
} }

View File

@ -150,6 +150,15 @@ func (p *RQLitePlugin) buildRR(qname string, record *DNSRecord) dns.RR {
Hdr: header, Hdr: header,
Txt: record.ParsedValue.([]string), Txt: record.ParsedValue.([]string),
} }
case dns.TypeNS:
return &dns.NS{
Hdr: header,
Ns: record.ParsedValue.(string),
}
case dns.TypeSOA:
soa := record.ParsedValue.(*dns.SOA)
soa.Hdr = header
return soa
default: default:
p.logger.Warn("Unsupported record type", p.logger.Warn("Unsupported record type",
zap.Uint16("type", record.Type), zap.Uint16("type", record.Type),

View File

@ -1,11 +1,15 @@
package installers package installers
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"time"
) )
const ( const (
@ -191,23 +195,26 @@ func (ci *CoreDNSInstaller) Install() error {
return nil return nil
} }
// Configure creates CoreDNS configuration files // Configure creates CoreDNS configuration files and seeds static DNS records into RQLite
func (ci *CoreDNSInstaller) Configure(domain string, rqliteDSN string, ns1IP, ns2IP, ns3IP string) error { func (ci *CoreDNSInstaller) Configure(domain string, rqliteDSN string, ns1IP, ns2IP, ns3IP string) error {
configDir := "/etc/coredns" configDir := "/etc/coredns"
if err := os.MkdirAll(configDir, 0755); err != nil { if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err) return fmt.Errorf("failed to create config directory: %w", err)
} }
// Create Corefile // Create Corefile (uses only RQLite plugin)
corefile := ci.generateCorefile(domain, rqliteDSN, configDir) corefile := ci.generateCorefile(domain, rqliteDSN)
if err := os.WriteFile(filepath.Join(configDir, "Corefile"), []byte(corefile), 0644); err != nil { if err := os.WriteFile(filepath.Join(configDir, "Corefile"), []byte(corefile), 0644); err != nil {
return fmt.Errorf("failed to write Corefile: %w", err) return fmt.Errorf("failed to write Corefile: %w", err)
} }
// Create zone file // Seed static DNS records into RQLite
zonefile := ci.generateZoneFile(domain, ns1IP, ns2IP, ns3IP) fmt.Fprintf(ci.logWriter, " Seeding static DNS records into RQLite...\n")
if err := os.WriteFile(filepath.Join(configDir, "db."+domain), []byte(zonefile), 0644); err != nil { if err := ci.seedStaticRecords(domain, rqliteDSN, ns1IP, ns2IP, ns3IP); err != nil {
return fmt.Errorf("failed to write zone file: %w", err) // Don't fail on seed errors - RQLite might not be up yet
fmt.Fprintf(ci.logWriter, " ⚠️ Could not seed DNS records (RQLite may not be ready): %v\n", err)
} else {
fmt.Fprintf(ci.logWriter, " ✓ Static DNS records seeded\n")
} }
return nil return nil
@ -263,14 +270,14 @@ rqlite:rqlite
` `
} }
// generateCorefile creates the CoreDNS configuration // generateCorefile creates the CoreDNS configuration (RQLite only)
func (ci *CoreDNSInstaller) generateCorefile(domain, rqliteDSN, configDir string) string { func (ci *CoreDNSInstaller) generateCorefile(domain, rqliteDSN string) string {
return fmt.Sprintf(`# CoreDNS configuration for %s return fmt.Sprintf(`# CoreDNS configuration for %s
# Uses RQLite for dynamic DNS records (deployments, ACME challenges) # Uses RQLite for ALL DNS records (static + dynamic)
# Falls back to static zone file for base records (SOA, NS) # Static records (SOA, NS, A) are seeded into RQLite during installation
%s { %s {
# First try RQLite for dynamic records (TXT for ACME, A for deployments) # RQLite handles all records: SOA, NS, A, TXT (ACME), etc.
rqlite { rqlite {
dsn %s dsn %s
refresh 5s refresh 5s
@ -278,9 +285,6 @@ func (ci *CoreDNSInstaller) generateCorefile(domain, rqliteDSN, configDir string
cache_size 10000 cache_size 10000
} }
# Fall back to static zone file for SOA/NS records
file %s/db.%s
# Enable logging and error reporting # Enable logging and error reporting
log log
errors errors
@ -293,44 +297,95 @@ func (ci *CoreDNSInstaller) generateCorefile(domain, rqliteDSN, configDir string
cache 300 cache 300
errors errors
} }
`, domain, domain, rqliteDSN, configDir, domain) `, domain, domain, rqliteDSN)
} }
// generateZoneFile creates the static DNS zone file // seedStaticRecords inserts static zone records into RQLite
func (ci *CoreDNSInstaller) generateZoneFile(domain, ns1IP, ns2IP, ns3IP string) string { func (ci *CoreDNSInstaller) seedStaticRecords(domain, rqliteDSN, ns1IP, ns2IP, ns3IP string) error {
return fmt.Sprintf(`$ORIGIN %s. // Generate serial based on current date
$TTL 300 serial := fmt.Sprintf("%d", time.Now().Unix())
@ IN SOA ns1.%s. admin.%s. ( // SOA record format: "mname rname serial refresh retry expire minimum"
2024012401 ; Serial soaValue := fmt.Sprintf("ns1.%s. admin.%s. %s 3600 1800 604800 300", domain, domain, serial)
3600 ; Refresh
1800 ; Retry
604800 ; Expire
300 ) ; Negative TTL
; Nameservers // Define all static records
@ IN NS ns1.%s. records := []struct {
@ IN NS ns2.%s. fqdn string
@ IN NS ns3.%s. recordType string
value string
ttl int
}{
// SOA record
{domain + ".", "SOA", soaValue, 300},
; Nameserver A records // NS records
ns1 IN A %s {domain + ".", "NS", "ns1." + domain + ".", 300},
ns2 IN A %s {domain + ".", "NS", "ns2." + domain + ".", 300},
ns3 IN A %s {domain + ".", "NS", "ns3." + domain + ".", 300},
; Root domain points to all nodes (round-robin) // Nameserver A records (glue)
@ IN A %s {"ns1." + domain + ".", "A", ns1IP, 300},
@ IN A %s {"ns2." + domain + ".", "A", ns2IP, 300},
@ IN A %s {"ns3." + domain + ".", "A", ns3IP, 300},
; Wildcard fallback (RQLite records take precedence for specific subdomains) // Root domain A records (round-robin)
* IN A %s {domain + ".", "A", ns1IP, 300},
* IN A %s {domain + ".", "A", ns2IP, 300},
* IN A %s {domain + ".", "A", ns3IP, 300},
`, domain, domain, domain, domain, domain, domain,
ns1IP, ns2IP, ns3IP, // Wildcard A records (round-robin)
ns1IP, ns2IP, ns3IP, {"*." + domain + ".", "A", ns1IP, 300},
ns1IP, ns2IP, ns3IP) {"*." + domain + ".", "A", ns2IP, 300},
{"*." + domain + ".", "A", ns3IP, 300},
}
// Build SQL statements
var statements []string
for _, r := range records {
// Use INSERT OR REPLACE to handle updates
stmt := fmt.Sprintf(
`INSERT OR REPLACE INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by) VALUES ('%s', '%s', '%s', %d, 'system', 'system')`,
r.fqdn, r.recordType, r.value, r.ttl,
)
statements = append(statements, stmt)
}
// Execute via RQLite HTTP API
return ci.executeRQLiteStatements(rqliteDSN, statements)
}
// executeRQLiteStatements executes SQL statements via RQLite HTTP API
func (ci *CoreDNSInstaller) executeRQLiteStatements(rqliteDSN string, statements []string) error {
// RQLite execute endpoint
executeURL := rqliteDSN + "/db/execute?pretty&timings"
// Build request body
body, err := json.Marshal(statements)
if err != nil {
return fmt.Errorf("failed to marshal statements: %w", err)
}
// Create request
req, err := http.NewRequest("POST", executeURL, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
// Execute with timeout
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("RQLite returned status %d: %s", resp.StatusCode, string(respBody))
}
return nil
} }
// containsLine checks if a string contains a specific line // containsLine checks if a string contains a specific line

View File

@ -457,15 +457,18 @@ func (h *DomainHandler) createDNSRecord(ctx context.Context, domain, deploymentI
// Create DNS A record // Create DNS A record
dnsQuery := ` dnsQuery := `
INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, deployment_id, node_id, created_by, created_at) INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, deployment_id, node_id, created_by, created_at, updated_at)
VALUES (?, 'A', ?, 300, ?, ?, ?, 'system', ?) VALUES (?, 'A', ?, 300, ?, ?, ?, 'system', ?, ?)
ON CONFLICT(fqdn) DO UPDATE SET value = ?, updated_at = ? ON CONFLICT(fqdn, record_type, value) DO UPDATE SET
deployment_id = excluded.deployment_id,
node_id = excluded.node_id,
updated_at = excluded.updated_at
` `
fqdn := domain + "." fqdn := domain + "."
now := time.Now() now := time.Now()
_, err = h.service.db.Exec(ctx, dnsQuery, fqdn, nodeIP, "", deploymentID, homeNodeID, now, nodeIP, now) _, err = h.service.db.Exec(ctx, dnsQuery, fqdn, nodeIP, "", deploymentID, homeNodeID, now, now)
if err != nil { if err != nil {
h.logger.Error("Failed to create DNS record", zap.Error(err)) h.logger.Error("Failed to create DNS record", zap.Error(err))
return return

View File

@ -324,7 +324,10 @@ func (s *DeploymentService) createDNSRecord(ctx context.Context, fqdn, recordTyp
query := ` query := `
INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, deployment_id, is_active, created_at, updated_at, created_by) INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, deployment_id, is_active, created_at, updated_at, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(fqdn) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at ON CONFLICT(fqdn, record_type, value) DO UPDATE SET
deployment_id = excluded.deployment_id,
updated_at = excluded.updated_at,
is_active = TRUE
` `
now := time.Now() now := time.Now()

View File

@ -0,0 +1,35 @@
#!/bin/bash
# Generates a tarball of the current codebase for deployment
# Output: /tmp/network-source.tar.gz
#
# Usage: ./scripts/generate-source-archive.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
OUTPUT="/tmp/network-source.tar.gz"
echo "Generating source archive..."
cd "$PROJECT_ROOT"
# Remove root-level binaries before archiving (they'll be rebuilt on VPS)
rm -f gateway cli node orama-cli-linux 2>/dev/null
tar czf "$OUTPUT" \
--exclude='.git' \
--exclude='node_modules' \
--exclude='*.log' \
--exclude='.DS_Store' \
--exclude='bin/' \
--exclude='dist/' \
--exclude='coverage/' \
--exclude='.claude/' \
--exclude='testdata/' \
--exclude='examples/' \
--exclude='*.tar.gz' \
.
echo "Archive created: $OUTPUT"
echo "Size: $(du -h $OUTPUT | cut -f1)"