Merge pull request 'chore: remove the decommissioned Proxmox DNS server and its migration script' (#622) from drop-decommissioned-dns into master
Reviewed-on: k.renner/homelab#622
This commit was merged in pull request #622.
This commit is contained in:
@@ -253,22 +253,6 @@ endpoints:
|
||||
{% endif %}
|
||||
|
||||
# Infrastructure
|
||||
- name: Proxmox DNS (VIP)
|
||||
group: infrastructure
|
||||
url: 10.2.40.53
|
||||
interval: 30s
|
||||
dns:
|
||||
query-name: k8s-1.srv.renner.uno
|
||||
query-type: A
|
||||
conditions:
|
||||
- "[DNS_RCODE] == NOERROR"
|
||||
- "[RESPONSE_TIME] < 500"
|
||||
alerts:
|
||||
- type: email
|
||||
{% if gatus_webhook_url %}
|
||||
- type: custom
|
||||
{% endif %}
|
||||
|
||||
- name: PostgreSQL HA (VIP)
|
||||
group: infrastructure
|
||||
url: tcp://10.2.40.135:5432
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# Proxmox DNS Server Configuration
|
||||
|
||||
# Proxmox API connection
|
||||
proxmox:
|
||||
host: "pve.renner.uno"
|
||||
user: "root@pam"
|
||||
|
||||
# Token-based authentication (recommended)
|
||||
token_name: "dns"
|
||||
token_value: "your-token-secret-here"
|
||||
|
||||
# OR password-based authentication
|
||||
# password: "your-password-here"
|
||||
|
||||
verify_ssl: false
|
||||
|
||||
# DNS server settings
|
||||
dns_host: "0.0.0.0" # Listen on all interfaces
|
||||
dns_port: 53 # Standard DNS port (requires root or CAP_NET_BIND_SERVICE)
|
||||
|
||||
# DNS suffix for VM/LXC hostnames
|
||||
dns_suffix: ".renner.uno"
|
||||
|
||||
# TTL for DNS responses (in seconds)
|
||||
dns_ttl: 300
|
||||
|
||||
# Cache settings
|
||||
cache_ttl: 60 # How often to refresh the cache (in seconds)
|
||||
|
||||
# Upstream DNS server for non-Proxmox queries
|
||||
upstream_dns: "10.2.40.1"
|
||||
@@ -1,15 +0,0 @@
|
||||
module proxmox-dns
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/miekg/dns v1.1.58
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
)
|
||||
@@ -1,16 +0,0 @@
|
||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1,565 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Proxmox struct {
|
||||
Host string `yaml:"host"`
|
||||
User string `yaml:"user"`
|
||||
TokenName string `yaml:"token_name"`
|
||||
TokenValue string `yaml:"token_value"`
|
||||
VerifySSL bool `yaml:"verify_ssl"`
|
||||
} `yaml:"proxmox"`
|
||||
DNSHost string `yaml:"dns_host"`
|
||||
DNSPort int `yaml:"dns_port"`
|
||||
DNSSuffix string `yaml:"dns_suffix"`
|
||||
DNSTTL int `yaml:"dns_ttl"`
|
||||
CacheTTL int `yaml:"cache_ttl"`
|
||||
UpstreamDNS string `yaml:"upstream_dns"`
|
||||
}
|
||||
|
||||
type DNSRecord struct {
|
||||
IPv4 string
|
||||
IPv6 string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type ProxmoxDNS struct {
|
||||
config Config
|
||||
cache map[string]*DNSRecord
|
||||
cacheMux sync.RWMutex
|
||||
client *http.Client
|
||||
stopChan chan struct{}
|
||||
nodes []string
|
||||
nodesMux sync.RWMutex
|
||||
}
|
||||
|
||||
func NewProxmoxDNS(configPath string) (*ProxmoxDNS, error) {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if config.DNSPort == 0 {
|
||||
config.DNSPort = 53
|
||||
}
|
||||
if config.CacheTTL == 0 {
|
||||
config.CacheTTL = 60
|
||||
}
|
||||
if config.DNSTTL == 0 {
|
||||
config.DNSTTL = 300
|
||||
}
|
||||
if config.UpstreamDNS == "" {
|
||||
config.UpstreamDNS = "10.2.40.1:53"
|
||||
}
|
||||
if !strings.Contains(config.UpstreamDNS, ":") {
|
||||
config.UpstreamDNS += ":53"
|
||||
}
|
||||
|
||||
return &ProxmoxDNS{
|
||||
config: config,
|
||||
cache: make(map[string]*DNSRecord),
|
||||
nodes: []string{},
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
},
|
||||
stopChan: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isValidIPv6ForDNS checks if an IPv6 address is suitable for DNS records
|
||||
func isValidIPv6ForDNS(ip net.IP) bool {
|
||||
if ip == nil || ip.To16() == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Reject loopback
|
||||
if ip.IsLoopback() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Reject link-local (fe80::/10) - THIS IS THE KEY FIX
|
||||
if ip.IsLinkLocalUnicast() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Reject multicast
|
||||
if ip.IsMulticast() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Reject unspecified
|
||||
if ip.IsUnspecified() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Accept global unicast and ULA (fc00::/7)
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ProxmoxDNS) queryProxmoxAPI(endpoint string) ([]byte, error) {
|
||||
// Remove port from host if present
|
||||
host := p.config.Proxmox.Host
|
||||
if !strings.Contains(host, ":") {
|
||||
host += ":8006"
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://%s/api2/json/%s", host, endpoint)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth := fmt.Sprintf("PVEAPIToken=%s!%s=%s",
|
||||
p.config.Proxmox.User,
|
||||
p.config.Proxmox.TokenName,
|
||||
p.config.Proxmox.TokenValue)
|
||||
req.Header.Set("Authorization", auth)
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (p *ProxmoxDNS) discoverNodes() error {
|
||||
data, err := p.queryProxmoxAPI("nodes")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to discover nodes: %w", err)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Data []struct {
|
||||
Node string `json:"node"`
|
||||
Status string `json:"status"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return fmt.Errorf("failed to parse nodes: %w", err)
|
||||
}
|
||||
|
||||
p.nodesMux.Lock()
|
||||
defer p.nodesMux.Unlock()
|
||||
|
||||
p.nodes = []string{}
|
||||
for _, node := range response.Data {
|
||||
if node.Status == "online" {
|
||||
p.nodes = append(p.nodes, node.Node)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Discovered %d online nodes: %v", len(p.nodes), p.nodes)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProxmoxDNS) refreshCache() {
|
||||
log.Println("Refreshing DNS cache...")
|
||||
|
||||
// Discover nodes first
|
||||
if err := p.discoverNodes(); err != nil {
|
||||
log.Printf("Failed to discover nodes: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
newCache := make(map[string]*DNSRecord)
|
||||
|
||||
p.nodesMux.RLock()
|
||||
nodes := append([]string{}, p.nodes...)
|
||||
p.nodesMux.RUnlock()
|
||||
|
||||
for _, node := range nodes {
|
||||
// Query VMs
|
||||
p.refreshVMs(node, newCache)
|
||||
// Query LXC containers
|
||||
p.refreshLXC(node, newCache)
|
||||
}
|
||||
|
||||
p.cacheMux.Lock()
|
||||
p.cache = newCache
|
||||
p.cacheMux.Unlock()
|
||||
|
||||
log.Printf("Cache refreshed: %d entries", len(newCache))
|
||||
}
|
||||
|
||||
func (p *ProxmoxDNS) refreshVMs(node string, cache map[string]*DNSRecord) {
|
||||
data, err := p.queryProxmoxAPI(fmt.Sprintf("nodes/%s/qemu", node))
|
||||
if err != nil {
|
||||
log.Printf("Failed to query VMs on %s: %v", node, err)
|
||||
return
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Data []struct {
|
||||
VMID int `json:"vmid"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
log.Printf("Failed to parse VM data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, vm := range response.Data {
|
||||
if vm.Status != "running" || vm.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Query agent network info
|
||||
agentData, err := p.queryProxmoxAPI(fmt.Sprintf("nodes/%s/qemu/%d/agent/network-get-interfaces", node, vm.VMID))
|
||||
if err != nil {
|
||||
log.Printf("VM %s (ID %d): no guest agent data", vm.Name, vm.VMID)
|
||||
continue
|
||||
}
|
||||
|
||||
var agentResp struct {
|
||||
Data struct {
|
||||
Result []struct {
|
||||
Name string `json:"name"`
|
||||
IPAddresses []struct {
|
||||
IPAddress string `json:"ip-address"`
|
||||
IPAddressType string `json:"ip-address-type"`
|
||||
} `json:"ip-addresses"`
|
||||
} `json:"result"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(agentData, &agentResp); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var ipv4, ipv6 string
|
||||
for _, iface := range agentResp.Data.Result {
|
||||
if iface.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ipAddr := range iface.IPAddresses {
|
||||
if ipAddr.IPAddressType == "ipv4" && ipv4 == "" {
|
||||
if !strings.HasPrefix(ipAddr.IPAddress, "127.") {
|
||||
ipv4 = ipAddr.IPAddress
|
||||
}
|
||||
} else if ipAddr.IPAddressType == "ipv6" && ipv6 == "" {
|
||||
ip := net.ParseIP(ipAddr.IPAddress)
|
||||
if isValidIPv6ForDNS(ip) {
|
||||
ipv6 = ipAddr.IPAddress
|
||||
log.Printf(" VM %s: accepting IPv6 %s", vm.Name, ipv6)
|
||||
} else if ip != nil {
|
||||
log.Printf(" VM %s: rejecting IPv6 %s (link-local/loopback/multicast)", vm.Name, ipAddr.IPAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop after finding both on first non-lo interface
|
||||
if ipv4 != "" && ipv6 != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ipv4 != "" || ipv6 != "" {
|
||||
hostname := strings.ToLower(vm.Name)
|
||||
// Strip DNS suffix from hostname if present
|
||||
hostname = strings.TrimSuffix(hostname, p.config.DNSSuffix)
|
||||
cache[hostname] = &DNSRecord{
|
||||
IPv4: ipv4,
|
||||
IPv6: ipv6,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
log.Printf("VM: %s -> IPv4=%s IPv6=%s", hostname, ipv4, ipv6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProxmoxDNS) refreshLXC(node string, cache map[string]*DNSRecord) {
|
||||
data, err := p.queryProxmoxAPI(fmt.Sprintf("nodes/%s/lxc", node))
|
||||
if err != nil {
|
||||
log.Printf("Failed to query LXC on %s: %v", node, err)
|
||||
return
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Data []struct {
|
||||
VMID int `json:"vmid"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
log.Printf("Failed to parse LXC data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, lxc := range response.Data {
|
||||
if lxc.Status != "running" || lxc.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Query LXC interfaces
|
||||
ifaceData, err := p.queryProxmoxAPI(fmt.Sprintf("nodes/%s/lxc/%d/interfaces", node, lxc.VMID))
|
||||
if err != nil {
|
||||
log.Printf("LXC %s (ID %d): failed to get interfaces", lxc.Name, lxc.VMID)
|
||||
continue
|
||||
}
|
||||
|
||||
var ifaceResp struct {
|
||||
Data []struct {
|
||||
Name string `json:"name"`
|
||||
Inet string `json:"inet"`
|
||||
Inet6 string `json:"inet6"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(ifaceData, &ifaceResp); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var ipv4, ipv6 string
|
||||
for _, iface := range ifaceResp.Data {
|
||||
if iface.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
|
||||
if iface.Inet != "" && ipv4 == "" {
|
||||
// Format is "IP/CIDR"
|
||||
parts := strings.Split(iface.Inet, "/")
|
||||
if len(parts) > 0 && !strings.HasPrefix(parts[0], "127.") {
|
||||
ipv4 = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
if iface.Inet6 != "" && ipv6 == "" {
|
||||
parts := strings.Split(iface.Inet6, "/")
|
||||
if len(parts) > 0 {
|
||||
ip := net.ParseIP(parts[0])
|
||||
if isValidIPv6ForDNS(ip) {
|
||||
ipv6 = parts[0]
|
||||
log.Printf(" LXC %s: accepting IPv6 %s", lxc.Name, ipv6)
|
||||
} else if ip != nil {
|
||||
log.Printf(" LXC %s: rejecting IPv6 %s (link-local/loopback/multicast)", lxc.Name, parts[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop after finding both on first non-lo interface
|
||||
if ipv4 != "" && ipv6 != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ipv4 != "" || ipv6 != "" {
|
||||
hostname := strings.ToLower(lxc.Name)
|
||||
// Strip DNS suffix from hostname if present
|
||||
hostname = strings.TrimSuffix(hostname, p.config.DNSSuffix)
|
||||
cache[hostname] = &DNSRecord{
|
||||
IPv4: ipv4,
|
||||
IPv6: ipv6,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
log.Printf("LXC: %s -> IPv4=%s IPv6=%s", hostname, ipv4, ipv6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProxmoxDNS) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Authoritative = true
|
||||
|
||||
for _, q := range r.Question {
|
||||
name := strings.ToLower(strings.TrimSuffix(q.Name, "."))
|
||||
qtype := dns.TypeToString[q.Qtype]
|
||||
|
||||
// Check if query is for our domain suffix
|
||||
if !strings.HasSuffix(name, strings.TrimPrefix(p.config.DNSSuffix, ".")) {
|
||||
// Forward to upstream
|
||||
log.Printf("Query %s (%s) - forwarding to upstream", name, qtype)
|
||||
p.forwardToUpstream(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract hostname (remove suffix)
|
||||
hostname := strings.TrimSuffix(name, p.config.DNSSuffix)
|
||||
|
||||
p.cacheMux.RLock()
|
||||
record, found := p.cache[hostname]
|
||||
p.cacheMux.RUnlock()
|
||||
|
||||
if !found {
|
||||
// NXDOMAIN
|
||||
m.SetRcode(r, dns.RcodeNameError)
|
||||
log.Printf("Query %s (%s) - NXDOMAIN", name, qtype)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
|
||||
switch q.Qtype {
|
||||
case dns.TypeA:
|
||||
if record.IPv4 != "" {
|
||||
rr := &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(p.config.DNSTTL),
|
||||
},
|
||||
A: net.ParseIP(record.IPv4),
|
||||
}
|
||||
m.Answer = append(m.Answer, rr)
|
||||
log.Printf("Query %s (A) -> %s", name, record.IPv4)
|
||||
} else {
|
||||
// NODATA (host exists but no A record)
|
||||
m.Ns = append(m.Ns, p.getSOARecord())
|
||||
log.Printf("Query %s (A) - NODATA", name)
|
||||
}
|
||||
|
||||
case dns.TypeAAAA:
|
||||
if record.IPv6 != "" {
|
||||
// Parse and validate IPv6 again before returning
|
||||
ip := net.ParseIP(record.IPv6)
|
||||
if ip != nil && isValidIPv6ForDNS(ip) {
|
||||
rr := &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(p.config.DNSTTL),
|
||||
},
|
||||
AAAA: ip,
|
||||
}
|
||||
m.Answer = append(m.Answer, rr)
|
||||
log.Printf("Query %s (AAAA) -> %s", name, record.IPv6)
|
||||
} else {
|
||||
// IPv6 is invalid (link-local etc)
|
||||
m.Ns = append(m.Ns, p.getSOARecord())
|
||||
log.Printf("Query %s (AAAA) - NODATA (invalid IPv6 in cache)", name)
|
||||
}
|
||||
} else {
|
||||
// NODATA (host exists but no AAAA record)
|
||||
m.Ns = append(m.Ns, p.getSOARecord())
|
||||
log.Printf("Query %s (AAAA) - NODATA", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
|
||||
func (p *ProxmoxDNS) getSOARecord() dns.RR {
|
||||
domain := strings.TrimPrefix(p.config.DNSSuffix, ".")
|
||||
soa := &dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: domain + ".",
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 300,
|
||||
},
|
||||
Ns: "ns." + domain + ".",
|
||||
Mbox: "admin." + domain + ".",
|
||||
Serial: uint32(time.Now().Unix()),
|
||||
Refresh: 3600,
|
||||
Retry: 600,
|
||||
Expire: 86400,
|
||||
Minttl: 300,
|
||||
}
|
||||
return soa
|
||||
}
|
||||
|
||||
func (p *ProxmoxDNS) forwardToUpstream(w dns.ResponseWriter, r *dns.Msg) {
|
||||
c := new(dns.Client)
|
||||
c.Timeout = 2 * time.Second
|
||||
|
||||
resp, _, err := c.Exchange(r, p.config.UpstreamDNS)
|
||||
if err != nil {
|
||||
log.Printf("Failed to forward to upstream: %v", err)
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(r, dns.RcodeServerFailure)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteMsg(resp)
|
||||
}
|
||||
|
||||
func (p *ProxmoxDNS) startCacheRefresher() {
|
||||
ticker := time.NewTicker(time.Duration(p.config.CacheTTL) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Initial refresh
|
||||
p.refreshCache()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
p.refreshCache()
|
||||
case <-p.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProxmoxDNS) Start() error {
|
||||
// Start cache refresher
|
||||
go p.startCacheRefresher()
|
||||
|
||||
// Start DNS server
|
||||
dns.HandleFunc(".", p.handleDNSRequest)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", p.config.DNSHost, p.config.DNSPort)
|
||||
log.Printf("Starting Proxmox DNS server on %s", addr)
|
||||
log.Printf("Base domain: %s", p.config.DNSSuffix)
|
||||
log.Printf("Upstream DNS: %s", p.config.UpstreamDNS)
|
||||
|
||||
server := &dns.Server{Addr: addr, Net: "udp"}
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("c", "/etc/proxmox-dns/config.yaml", "Path to configuration file")
|
||||
flag.Parse()
|
||||
|
||||
server, err := NewProxmoxDNS(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create DNS server: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Proxmox DNS Server starting...")
|
||||
if err := server.Start(); err != nil {
|
||||
log.Fatalf("Failed to start DNS server: %v", err)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,27 +0,0 @@
|
||||
[Unit]
|
||||
Description=Proxmox DNS Server
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/opt/proxmox-dns
|
||||
ExecStart=/usr/bin/python3 /opt/proxmox-dns/proxmox_dns.py -c /etc/proxmox-dns/config.yaml
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/log
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script to help with UniFi client migration
|
||||
#
|
||||
# NOTE: This script is NO LONGER RECOMMENDED
|
||||
# Instead, follow the migration guide in MIGRATION_QUICKSTART.md
|
||||
#
|
||||
# This script removes unifi_client resources from state, but the recommended
|
||||
# approach is to let 'tofu apply' handle the removal automatically.
|
||||
|
||||
echo "⚠️ WARNING: This script is deprecated"
|
||||
echo ""
|
||||
echo "Please use the recommended migration path instead:"
|
||||
echo " 1. Deploy DNS server: tofu apply -target=module.proxmox_dns"
|
||||
echo " 2. Configure UniFi DNS to use 10.2.40.53"
|
||||
echo " 3. Run full apply: tofu apply"
|
||||
echo ""
|
||||
echo "See MIGRATION_QUICKSTART.md for details."
|
||||
echo ""
|
||||
read -p "Do you still want to remove resources from state? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Cancelled. See MIGRATION_QUICKSTART.md for recommended approach."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
echo "Removing unifi_client resources from OpenTofu state..."
|
||||
echo "Note: VMs will keep their current network configuration"
|
||||
echo ""
|
||||
|
||||
# List of all unifi_client resources to remove
|
||||
RESOURCES=(
|
||||
"module.cdns_srv_renner_uno.unifi_client.this[0]"
|
||||
"module.fortress_renner_uno.unifi_client.this[0]"
|
||||
"module.grafana_srv_renner_uno.unifi_client.this[0]"
|
||||
"module.influxdb_srv_renner_uno.unifi_client.this[0]"
|
||||
"module.kasm_srv_renner_uno.unifi_client.this[0]"
|
||||
"module.mosquitto_srv_renner_uno.unifi_client.this[0]"
|
||||
"module.netbox_srv_renner_uno.unifi_client.this[0]"
|
||||
"module.ntp_srv_renner_uno.unifi_client.this[0]"
|
||||
"module.paperless_srv_renner_uno.unifi_client.this[0]"
|
||||
"module.uptime_kuma_srv_renner_uno.unifi_client.this[0]"
|
||||
)
|
||||
|
||||
for resource in "${RESOURCES[@]}"; do
|
||||
echo "Removing: $resource"
|
||||
tofu state rm "$resource" || echo " (already removed or not found)"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Done! Run 'tofu plan' to verify."
|
||||
Reference in New Issue
Block a user