mirror of
https://github.com/aauren/tailscale-bind-ddns.git
synced 2026-10-24 21:02:21 +00:00
803 lines
20 KiB
Go
803 lines
20 KiB
Go
package bind
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aauren/tailscale-bind-ddns/pkg/config"
|
|
"github.com/miekg/dns"
|
|
"k8s.io/klog/v2"
|
|
)
|
|
|
|
// IPv4 subnet size constants
|
|
const (
|
|
IPv4Subnet8 = 8 // /8 subnet
|
|
IPv4Subnet16 = 16 // /16 subnet
|
|
IPv4Subnet24 = 24 // /24 subnet
|
|
)
|
|
|
|
// IPv6 subnet size constants
|
|
const (
|
|
IPv6Subnet32 = 32 // /32 subnet
|
|
IPv6Subnet48 = 48 // /48 subnet
|
|
IPv6Subnet64 = 64 // /64 subnet
|
|
)
|
|
|
|
// IPv6 nibble constants
|
|
const (
|
|
IPv6NibbleMaskLow = 0x0f // Low nibble mask
|
|
IPv6NibbleMaskHigh = 0xf0 // High nibble mask
|
|
IPv6NibbleShift = 4 // Nibble shift amount
|
|
)
|
|
|
|
const tsigDefaultTimeout = 300
|
|
|
|
type axfrFunc func(ctx context.Context, zone string) ([]dns.RR, error)
|
|
|
|
// Client represents a Bind DDNS client
|
|
type Client struct {
|
|
server string
|
|
port int
|
|
zone string
|
|
keyName string
|
|
keySecret string
|
|
algorithm string
|
|
ttl uint32
|
|
|
|
// PTR configuration
|
|
ptrConfig *config.PTRConfig
|
|
|
|
axfr axfrFunc
|
|
}
|
|
|
|
// DNSRecord represents a DNS record (A or PTR)
|
|
type DNSRecord struct {
|
|
Name string
|
|
Value string
|
|
TTL uint32
|
|
Type string // "A", "AAAA", or "PTR"
|
|
// Delete indicates that the record should be removed without inserting a replacement.
|
|
Delete bool
|
|
}
|
|
|
|
// NewClient creates a new Bind DDNS client
|
|
func NewClient(
|
|
server string,
|
|
port int,
|
|
zone, keyName, keySecret, algorithm string,
|
|
ttl time.Duration,
|
|
ptrConfig *config.PTRConfig,
|
|
) (*Client, error) {
|
|
if server == "" {
|
|
return nil, fmt.Errorf("server is required")
|
|
}
|
|
if zone == "" {
|
|
return nil, fmt.Errorf("zone is required")
|
|
}
|
|
if keyName == "" {
|
|
return nil, fmt.Errorf("key name is required")
|
|
}
|
|
if keySecret == "" {
|
|
return nil, fmt.Errorf("key secret is required")
|
|
}
|
|
|
|
client := &Client{
|
|
server: server,
|
|
port: port,
|
|
zone: zone,
|
|
keyName: keyName,
|
|
keySecret: keySecret,
|
|
algorithm: algorithm,
|
|
ttl: uint32(ttl.Seconds()),
|
|
ptrConfig: ptrConfig,
|
|
}
|
|
client.init()
|
|
return client, nil
|
|
}
|
|
|
|
// init injects defaults that require the client instance.
|
|
func (c *Client) init() {
|
|
if c.axfr == nil {
|
|
c.axfr = c.axfrTransfer
|
|
}
|
|
}
|
|
|
|
func (c *Client) serverAddress() string {
|
|
return net.JoinHostPort(c.server, fmt.Sprintf("%d", c.port))
|
|
}
|
|
|
|
// FetchInventory retrieves the current DNS records from the managed zones via AXFR.
|
|
func (c *Client) FetchInventory(ctx context.Context) ([]DNSRecord, error) {
|
|
c.init()
|
|
|
|
var inventory []DNSRecord
|
|
|
|
forwardRecords, err := c.collectZoneRecords(ctx, c.zone, func(rr dns.RR) *DNSRecord {
|
|
switch v := rr.(type) {
|
|
case *dns.A:
|
|
name := trimZoneSuffix(v.Hdr.Name, c.zone)
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
return &DNSRecord{
|
|
Name: name,
|
|
Value: v.A.String(),
|
|
TTL: v.Hdr.Ttl,
|
|
Type: "A",
|
|
}
|
|
case *dns.AAAA:
|
|
name := trimZoneSuffix(v.Hdr.Name, c.zone)
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
return &DNSRecord{
|
|
Name: name,
|
|
Value: v.AAAA.String(),
|
|
TTL: v.Hdr.Ttl,
|
|
Type: "AAAA",
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetching zone %s: %w", c.zone, err)
|
|
}
|
|
inventory = append(inventory, forwardRecords...)
|
|
|
|
if c.ptrConfig != nil && c.ptrConfig.Enabled {
|
|
if c.ptrConfig.IPv4Zone != "" {
|
|
ipv4Records, err := c.collectZoneRecords(ctx, c.ptrConfig.IPv4Zone, func(rr dns.RR) *DNSRecord {
|
|
ptr, ok := rr.(*dns.PTR)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &DNSRecord{
|
|
Name: dns.Fqdn(ptr.Hdr.Name),
|
|
Value: strings.TrimSuffix(ptr.Ptr, "."),
|
|
TTL: ptr.Hdr.Ttl,
|
|
Type: "PTR",
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetching IPv4 PTR zone %s: %w", c.ptrConfig.IPv4Zone, err)
|
|
}
|
|
inventory = append(inventory, ipv4Records...)
|
|
}
|
|
|
|
if c.ptrConfig.IPv6Enabled && c.ptrConfig.IPv6Zone != "" {
|
|
ipv6Records, err := c.collectZoneRecords(ctx, c.ptrConfig.IPv6Zone, func(rr dns.RR) *DNSRecord {
|
|
ptr, ok := rr.(*dns.PTR)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &DNSRecord{
|
|
Name: dns.Fqdn(ptr.Hdr.Name),
|
|
Value: strings.TrimSuffix(ptr.Ptr, "."),
|
|
TTL: ptr.Hdr.Ttl,
|
|
Type: "PTR",
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetching IPv6 PTR zone %s: %w", c.ptrConfig.IPv6Zone, err)
|
|
}
|
|
inventory = append(inventory, ipv6Records...)
|
|
}
|
|
}
|
|
|
|
return inventory, nil
|
|
}
|
|
|
|
func (c *Client) collectZoneRecords(ctx context.Context,
|
|
zone string,
|
|
normalize func(dns.RR) *DNSRecord) ([]DNSRecord, error) {
|
|
if zone == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
rrs, err := c.axfr(ctx, zone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var records []DNSRecord
|
|
for _, rr := range rrs {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
record := normalize(rr)
|
|
if record == nil {
|
|
continue
|
|
}
|
|
records = append(records, *record)
|
|
}
|
|
|
|
return records, nil
|
|
}
|
|
|
|
func trimZoneSuffix(name, zone string) string {
|
|
name = strings.TrimSuffix(name, ".")
|
|
zoneFqdn := strings.TrimSuffix(dns.Fqdn(zone), ".")
|
|
|
|
if name == zoneFqdn {
|
|
return ""
|
|
}
|
|
|
|
suffix := "." + zoneFqdn
|
|
return strings.TrimSuffix(name, suffix)
|
|
}
|
|
|
|
func (c *Client) axfrTransfer(ctx context.Context, zone string) ([]dns.RR, error) {
|
|
key, err := c.createTSIGKey()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating TSIG key: %w", err)
|
|
}
|
|
|
|
msg := new(dns.Msg)
|
|
msg.SetAxfr(dns.Fqdn(zone))
|
|
msg.SetTsig(key.Hdr.Name, key.Algorithm, tsigDefaultTimeout, time.Now().Unix())
|
|
|
|
transfer := &dns.Transfer{
|
|
DialTimeout: 5 * time.Second,
|
|
ReadTimeout: 5 * time.Second,
|
|
}
|
|
transfer.TsigSecret = map[string]string{key.Hdr.Name: c.keySecret}
|
|
|
|
envChan, err := transfer.In(msg, c.serverAddress())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("requesting AXFR: %w", err)
|
|
}
|
|
|
|
var records []dns.RR
|
|
for env := range envChan {
|
|
if env.Error != nil {
|
|
return nil, fmt.Errorf("AXFR envelope error: %w", env.Error)
|
|
}
|
|
records = append(records, env.RR...)
|
|
}
|
|
|
|
return records, nil
|
|
}
|
|
|
|
// UpdateRecords updates DNS records for the given machines
|
|
func (c *Client) UpdateRecords(ctx context.Context, records []DNSRecord, dryRun bool) error {
|
|
c.init()
|
|
|
|
if dryRun {
|
|
klog.Infof("DRY RUN: Would update %d DNS records", len(records))
|
|
for _, record := range records {
|
|
action := "create/update"
|
|
if record.Delete {
|
|
action = "delete"
|
|
}
|
|
switch record.Type {
|
|
case "PTR":
|
|
klog.V(1).Infof("DRY RUN: Would %s PTR record %s -> %s (TTL: %d)",
|
|
action, record.Name, record.Value, record.TTL)
|
|
case "AAAA":
|
|
klog.V(1).Infof("DRY RUN: Would %s AAAA record %s.%s -> %s (TTL: %d)",
|
|
action, record.Name, c.zone, record.Value, record.TTL)
|
|
default:
|
|
klog.V(1).Infof("DRY RUN: Would %s A record %s.%s -> %s (TTL: %d)",
|
|
action, record.Name, c.zone, record.Value, record.TTL)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if len(records) == 0 {
|
|
klog.V(1).Info("No records to update")
|
|
return nil
|
|
}
|
|
|
|
klog.Infof("Updating %d DNS records", len(records))
|
|
|
|
// Group records by zone
|
|
recordsByZone := make(map[string][]DNSRecord)
|
|
for _, record := range records {
|
|
var zone string
|
|
if record.Type == "PTR" {
|
|
// For PTR records, determine the zone dynamically based on the record name
|
|
if strings.Contains(record.Name, ".in-addr.arpa.") {
|
|
// IPv4 PTR record - extract zone from the record name
|
|
zone = c.extractIPv4ZoneFromPTRName(record.Name)
|
|
} else if strings.Contains(record.Name, ".ip6.arpa.") {
|
|
// IPv6 PTR record - extract zone from the record name
|
|
zone = c.extractIPv6ZoneFromPTRName(record.Name)
|
|
}
|
|
} else {
|
|
// A/AAAA records go to the main zone
|
|
zone = c.zone
|
|
}
|
|
|
|
if zone != "" {
|
|
recordsByZone[zone] = append(recordsByZone[zone], record)
|
|
}
|
|
}
|
|
|
|
// Create TSIG key
|
|
key, err := c.createTSIGKey()
|
|
if err != nil {
|
|
return fmt.Errorf("creating TSIG key: %w", err)
|
|
}
|
|
|
|
// Send updates for each zone
|
|
for zone, zoneRecords := range recordsByZone {
|
|
klog.V(1).Infof("Sending %d records to zone %s", len(zoneRecords), zone)
|
|
|
|
if err := c.sendZoneUpdate(ctx, zone, zoneRecords, key); err != nil {
|
|
return fmt.Errorf("sending update to zone %s: %w", zone, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// sendZoneUpdate sends DNS updates for a specific zone
|
|
func (c *Client) sendZoneUpdate(ctx context.Context, zone string, records []DNSRecord, key *dns.TSIG) error {
|
|
// Create dynamic update message for this zone
|
|
msg := new(dns.Msg)
|
|
msg.SetUpdate(dns.Fqdn(zone))
|
|
|
|
// Add records to the update message
|
|
klog.V(2).Infof("Adding %d records to zone %s", len(records), zone)
|
|
for _, record := range records {
|
|
if record.Type == "PTR" {
|
|
// Handle PTR records
|
|
if record.Delete {
|
|
klog.V(1).Infof("Deleting PTR record: %s", record.Name)
|
|
} else {
|
|
klog.V(1).Infof("Processing PTR record: %s -> %s", record.Name, record.Value)
|
|
}
|
|
|
|
// Remove any existing PTR record for this name
|
|
rrset := &dns.PTR{
|
|
Hdr: dns.RR_Header{
|
|
Name: dns.Fqdn(record.Name),
|
|
Rrtype: dns.TypePTR,
|
|
Class: dns.ClassINET,
|
|
Ttl: 0, // TTL 0 for removal
|
|
},
|
|
}
|
|
msg.RemoveRRset([]dns.RR{rrset})
|
|
|
|
if record.Delete {
|
|
continue
|
|
}
|
|
|
|
// Add new PTR record
|
|
ptrRecord := &dns.PTR{
|
|
Hdr: dns.RR_Header{
|
|
Name: dns.Fqdn(record.Name),
|
|
Rrtype: dns.TypePTR,
|
|
Class: dns.ClassINET,
|
|
Ttl: record.TTL,
|
|
},
|
|
Ptr: dns.Fqdn(record.Value),
|
|
}
|
|
msg.Insert([]dns.RR{ptrRecord})
|
|
|
|
} else {
|
|
action := "Processing"
|
|
if record.Delete {
|
|
action = "Deleting"
|
|
}
|
|
klog.V(1).Infof("%s %s record: %s.%s -> %s", action, record.Type, record.Name, zone, record.Value)
|
|
|
|
// Remove any existing A/AAAA record for this name
|
|
var rrset dns.RR
|
|
if record.Type == "AAAA" {
|
|
rrset = &dns.AAAA{
|
|
Hdr: dns.RR_Header{
|
|
Name: dns.Fqdn(record.Name + "." + zone),
|
|
Rrtype: dns.TypeAAAA,
|
|
Class: dns.ClassINET,
|
|
Ttl: 0, // TTL 0 for removal
|
|
},
|
|
}
|
|
} else {
|
|
rrset = &dns.A{
|
|
Hdr: dns.RR_Header{
|
|
Name: dns.Fqdn(record.Name + "." + zone),
|
|
Rrtype: dns.TypeA,
|
|
Class: dns.ClassINET,
|
|
Ttl: 0, // TTL 0 for removal
|
|
},
|
|
}
|
|
}
|
|
msg.RemoveRRset([]dns.RR{rrset})
|
|
|
|
if record.Delete {
|
|
continue
|
|
}
|
|
|
|
// Add new A/AAAA record
|
|
var newRecord dns.RR
|
|
if record.Type == "AAAA" {
|
|
newRecord = &dns.AAAA{
|
|
Hdr: dns.RR_Header{
|
|
Name: dns.Fqdn(record.Name + "." + zone),
|
|
Rrtype: dns.TypeAAAA,
|
|
Class: dns.ClassINET,
|
|
Ttl: record.TTL,
|
|
},
|
|
AAAA: net.ParseIP(record.Value),
|
|
}
|
|
} else {
|
|
newRecord = &dns.A{
|
|
Hdr: dns.RR_Header{
|
|
Name: dns.Fqdn(record.Name + "." + zone),
|
|
Rrtype: dns.TypeA,
|
|
Class: dns.ClassINET,
|
|
Ttl: record.TTL,
|
|
},
|
|
A: net.ParseIP(record.Value),
|
|
}
|
|
}
|
|
msg.Insert([]dns.RR{newRecord})
|
|
}
|
|
}
|
|
|
|
// Sign the message with TSIG (300 seconds timeout)
|
|
const tsigTimeout = 300
|
|
msg.SetTsig(key.Hdr.Name, key.Algorithm, tsigTimeout, time.Now().Unix())
|
|
|
|
// Send the update
|
|
klog.V(2).Infof("Sending DNS update message to zone %s: %s", zone, msg.String())
|
|
klog.V(2).Infof("Message Question Section: %v", msg.Question)
|
|
|
|
client := new(dns.Client)
|
|
client.TsigSecret = map[string]string{key.Hdr.Name: c.keySecret}
|
|
|
|
response, _, err := client.ExchangeContext(ctx, msg, net.JoinHostPort(c.server, fmt.Sprintf("%d", c.port)))
|
|
if err != nil {
|
|
return fmt.Errorf("sending DNS update: %w", err)
|
|
}
|
|
|
|
if response.Rcode != dns.RcodeSuccess {
|
|
return fmt.Errorf("DNS update failed with Rcode %d: %s", response.Rcode, dns.RcodeToString[response.Rcode])
|
|
}
|
|
|
|
klog.V(1).Infof("Successfully updated %d records in zone %s", len(records), zone)
|
|
return nil
|
|
}
|
|
|
|
// createTSIGKey creates a TSIG key for authentication
|
|
func (c *Client) createTSIGKey() (*dns.TSIG, error) {
|
|
algorithm := c.algorithm
|
|
if algorithm == "" {
|
|
algorithm = "hmac-sha256"
|
|
}
|
|
|
|
// Validate algorithm
|
|
switch algorithm {
|
|
case "hmac-md5", "hmac-sha1", "hmac-sha256", "hmac-sha384", "hmac-sha512":
|
|
// Valid algorithms
|
|
default:
|
|
return nil, fmt.Errorf("unsupported TSIG algorithm: %s", algorithm)
|
|
}
|
|
|
|
return &dns.TSIG{
|
|
Hdr: dns.RR_Header{
|
|
Name: c.keyName,
|
|
Rrtype: dns.TypeTSIG,
|
|
Class: dns.ClassANY,
|
|
},
|
|
Algorithm: fmt.Sprintf("%s.", algorithm),
|
|
}, nil
|
|
}
|
|
|
|
// ValidateConnection tests the connection to the Bind server
|
|
func (c *Client) ValidateConnection(ctx context.Context) error {
|
|
klog.V(1).Infof("Validating connection to Bind server %s:%d", c.server, c.port)
|
|
|
|
// Create a simple query to test connectivity
|
|
msg := new(dns.Msg)
|
|
msg.SetQuestion(dns.Fqdn(c.zone), dns.TypeSOA)
|
|
|
|
client := new(dns.Client)
|
|
client.Timeout = 5 * time.Second
|
|
|
|
response, _, err := client.ExchangeContext(ctx, msg, net.JoinHostPort(c.server, fmt.Sprintf("%d", c.port)))
|
|
if err != nil {
|
|
return fmt.Errorf("connection test failed: %w", err)
|
|
}
|
|
|
|
if response.Rcode != dns.RcodeSuccess {
|
|
return fmt.Errorf("connection test failed with Rcode %d: %s", response.Rcode, dns.RcodeToString[response.Rcode])
|
|
}
|
|
|
|
klog.V(1).Info("Successfully validated connection to Bind server")
|
|
return nil
|
|
}
|
|
|
|
// StartUpdating starts the DDNS update process
|
|
func (c *Client) StartUpdating(
|
|
ctx context.Context,
|
|
updateInterval time.Duration,
|
|
recordChan <-chan []DNSRecord,
|
|
dryRun bool,
|
|
) {
|
|
ticker := time.NewTicker(updateInterval)
|
|
defer ticker.Stop()
|
|
|
|
klog.Infof("Starting DDNS updates with interval %v", updateInterval)
|
|
|
|
// Process initial records if available
|
|
select {
|
|
case records := <-recordChan:
|
|
if err := c.UpdateRecords(ctx, records, dryRun); err != nil {
|
|
klog.Errorf("Failed to update initial records: %v", err)
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case records := <-recordChan:
|
|
if err := c.UpdateRecords(ctx, records, dryRun); err != nil {
|
|
klog.Errorf("Failed to update records: %v", err)
|
|
}
|
|
|
|
case <-ticker.C:
|
|
// Periodic update - check if there are any pending records
|
|
select {
|
|
case records := <-recordChan:
|
|
if err := c.UpdateRecords(ctx, records, dryRun); err != nil {
|
|
klog.Errorf("Failed to update records: %v", err)
|
|
}
|
|
default:
|
|
// No records to process
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
klog.Info("DDNS updating stopped")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// isIPInSubnet checks if an IP address is within the specified subnet
|
|
func isIPInSubnet(ipStr, subnetStr string) bool {
|
|
if subnetStr == "" {
|
|
return false
|
|
}
|
|
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
|
|
_, network, err := net.ParseCIDR(subnetStr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return network.Contains(ip)
|
|
}
|
|
|
|
// generateIPv4PTRZone generates the PTR zone name for IPv4 based on subnet size
|
|
func (c *Client) generateIPv4PTRZone(ipStr string, subnetSize int) (string, error) {
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil || ip.To4() == nil {
|
|
return "", fmt.Errorf("invalid IPv4 address: %s", ipStr)
|
|
}
|
|
|
|
ipv4 := ip.To4()
|
|
|
|
switch subnetSize {
|
|
case IPv4Subnet8:
|
|
// /8: Use first octet (e.g., 100.64.0.1 -> 100.in-addr.arpa)
|
|
return fmt.Sprintf("%d.in-addr.arpa", ipv4[0]), nil
|
|
case IPv4Subnet16:
|
|
// /16: Use first two octets (e.g., 100.64.0.1 -> 64.100.in-addr.arpa)
|
|
return fmt.Sprintf("%d.%d.in-addr.arpa", ipv4[1], ipv4[0]), nil
|
|
case IPv4Subnet24:
|
|
// /24: Use first three octets (e.g., 100.64.0.1 -> 0.64.100.in-addr.arpa)
|
|
return fmt.Sprintf("%d.%d.%d.in-addr.arpa", ipv4[2], ipv4[1], ipv4[0]), nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported IPv4 subnet size: %d", subnetSize)
|
|
}
|
|
}
|
|
|
|
// generateIPv6PTRZone generates the PTR zone name for IPv6 based on subnet size
|
|
func (c *Client) generateIPv6PTRZone(ipStr string, subnetSize int) (string, error) {
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil || ip.To4() != nil {
|
|
return "", fmt.Errorf("invalid IPv6 address: %s", ipStr)
|
|
}
|
|
|
|
// Convert IPv6 to nibble format
|
|
nibbles := ipv6ToReverseDNS(ipStr)
|
|
if nibbles == "" {
|
|
return "", fmt.Errorf("failed to convert IPv6 to reverse DNS format")
|
|
}
|
|
|
|
// Remove the trailing dot and .ip6.arpa
|
|
nibbles = strings.TrimSuffix(nibbles, ".ip6.arpa.")
|
|
|
|
// Calculate how many nibbles to use based on subnet size
|
|
var nibblesToUse int
|
|
switch subnetSize {
|
|
case IPv6Subnet32:
|
|
nibblesToUse = 8 // /32 = 8 nibbles
|
|
case IPv6Subnet48:
|
|
nibblesToUse = 12 // /48 = 12 nibbles
|
|
case IPv6Subnet64:
|
|
nibblesToUse = 16 // /64 = 16 nibbles
|
|
default:
|
|
return "", fmt.Errorf("unsupported IPv6 subnet size: %d", subnetSize)
|
|
}
|
|
|
|
// Split nibbles and take the required number from the end (network portion)
|
|
nibbleParts := strings.Split(nibbles, ".")
|
|
if len(nibbleParts) < nibblesToUse {
|
|
return "", fmt.Errorf("not enough nibbles for subnet size %d", subnetSize)
|
|
}
|
|
|
|
// Take the last nibblesToUse nibbles (network portion)
|
|
selectedNibbles := nibbleParts[len(nibbleParts)-nibblesToUse:]
|
|
|
|
return strings.Join(selectedNibbles, ".") + ".ip6.arpa", nil
|
|
}
|
|
|
|
// extractIPv4ZoneFromPTRName extracts the zone name from an IPv4 PTR record name
|
|
func (c *Client) extractIPv4ZoneFromPTRName(ptrName string) string {
|
|
if c.ptrConfig == nil || !c.ptrConfig.Enabled {
|
|
return ""
|
|
}
|
|
|
|
// Remove trailing dot and .in-addr.arpa
|
|
name := strings.TrimSuffix(ptrName, ".in-addr.arpa.")
|
|
name = strings.TrimSuffix(name, ".")
|
|
|
|
// Split by dots to get octets
|
|
octets := strings.Split(name, ".")
|
|
if len(octets) != 4 {
|
|
return ""
|
|
}
|
|
|
|
// Generate zone based on subnet size
|
|
switch c.ptrConfig.IPv4SubnetSize {
|
|
case IPv4Subnet8:
|
|
// /8: Use first octet (e.g., 4.3.2.1 -> 1.in-addr.arpa)
|
|
return fmt.Sprintf("%s.in-addr.arpa", octets[3])
|
|
case IPv4Subnet16:
|
|
// /16: Use first two octets (e.g., 4.3.2.1 -> 2.1.in-addr.arpa)
|
|
return fmt.Sprintf("%s.%s.in-addr.arpa", octets[2], octets[3])
|
|
case IPv4Subnet24:
|
|
// /24: Use first three octets (e.g., 4.3.2.1 -> 3.2.1.in-addr.arpa)
|
|
return fmt.Sprintf("%s.%s.%s.in-addr.arpa", octets[1], octets[2], octets[3])
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// extractIPv6ZoneFromPTRName extracts the zone name from an IPv6 PTR record name
|
|
func (c *Client) extractIPv6ZoneFromPTRName(ptrName string) string {
|
|
if c.ptrConfig == nil || !c.ptrConfig.Enabled || !c.ptrConfig.IPv6Enabled {
|
|
return ""
|
|
}
|
|
|
|
// Remove trailing dot and .ip6.arpa
|
|
name := strings.TrimSuffix(ptrName, ".ip6.arpa.")
|
|
name = strings.TrimSuffix(name, ".")
|
|
|
|
// Split by dots to get nibbles
|
|
nibbles := strings.Split(name, ".")
|
|
if len(nibbles) < 8 {
|
|
return ""
|
|
}
|
|
|
|
// Calculate how many nibbles to use based on subnet size
|
|
var nibblesToUse int
|
|
switch c.ptrConfig.IPv6SubnetSize {
|
|
case IPv6Subnet32:
|
|
nibblesToUse = 8 // /32 = 8 nibbles
|
|
case IPv6Subnet48:
|
|
nibblesToUse = 12 // /48 = 12 nibbles
|
|
case IPv6Subnet64:
|
|
nibblesToUse = 16 // /64 = 16 nibbles
|
|
default:
|
|
return ""
|
|
}
|
|
|
|
if len(nibbles) < nibblesToUse {
|
|
return ""
|
|
}
|
|
|
|
// Take the last nibblesToUse nibbles (network portion)
|
|
selectedNibbles := nibbles[len(nibbles)-nibblesToUse:]
|
|
|
|
return strings.Join(selectedNibbles, ".") + ".ip6.arpa"
|
|
}
|
|
|
|
// CreatePTRRecord creates a PTR record for the given IP address and hostname
|
|
func (c *Client) CreatePTRRecord(ipStr, hostname string) (*DNSRecord, error) {
|
|
if c.ptrConfig == nil || !c.ptrConfig.Enabled {
|
|
return nil, nil
|
|
}
|
|
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil {
|
|
return nil, fmt.Errorf("invalid IP address: %s", ipStr)
|
|
}
|
|
|
|
var ptrName string
|
|
var subnet string
|
|
|
|
if ip.To4() != nil {
|
|
// IPv4 address
|
|
if !isIPInSubnet(ipStr, c.ptrConfig.IPv4Subnet) {
|
|
klog.Warningf("IPv4 address %s is not in configured subnet %s, skipping PTR record", ipStr, c.ptrConfig.IPv4Subnet)
|
|
return nil, nil
|
|
}
|
|
|
|
// Create reverse DNS name for IPv4 (e.g., 1.2.3.4 -> 4.3.2.1.in-addr.arpa.)
|
|
parts := strings.Split(ipStr, ".")
|
|
if len(parts) != 4 {
|
|
return nil, fmt.Errorf("invalid IPv4 address format: %s", ipStr)
|
|
}
|
|
|
|
// Reverse the IP address parts
|
|
ptrName = fmt.Sprintf("%s.%s.%s.%s.in-addr.arpa.", parts[3], parts[2], parts[1], parts[0])
|
|
subnet = c.ptrConfig.IPv4Subnet
|
|
|
|
} else {
|
|
// IPv6 address
|
|
if !c.ptrConfig.IPv6Enabled {
|
|
klog.V(2).Infof("IPv6 PTR records disabled, skipping IPv6 address %s", ipStr)
|
|
return nil, nil
|
|
}
|
|
|
|
if !isIPInSubnet(ipStr, c.ptrConfig.IPv6Subnet) {
|
|
klog.Warningf("IPv6 address %s is not in configured subnet %s, skipping PTR record", ipStr, c.ptrConfig.IPv6Subnet)
|
|
return nil, nil
|
|
}
|
|
|
|
// Create reverse DNS name for IPv6
|
|
ptrName = ipv6ToReverseDNS(ipStr)
|
|
subnet = c.ptrConfig.IPv6Subnet
|
|
}
|
|
|
|
klog.V(2).Infof("Creating PTR record for %s -> %s (subnet: %s)", ptrName, hostname, subnet)
|
|
|
|
return &DNSRecord{
|
|
Name: ptrName,
|
|
Value: hostname,
|
|
TTL: c.ttl,
|
|
Type: "PTR",
|
|
}, nil
|
|
}
|
|
|
|
// ipv6ToReverseDNS converts an IPv6 address to reverse DNS format
|
|
func ipv6ToReverseDNS(ipStr string) string {
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil {
|
|
return ""
|
|
}
|
|
|
|
// Convert to 16-byte representation
|
|
ipv6 := ip.To16()
|
|
if ipv6 == nil {
|
|
return ""
|
|
}
|
|
|
|
// Convert each byte to two nibbles and reverse the order
|
|
var nibbles []string
|
|
for _, i := range slices.Backward(ipv6) {
|
|
nibbles = append(nibbles, fmt.Sprintf("%x", i&IPv6NibbleMaskLow))
|
|
nibbles = append(nibbles, fmt.Sprintf("%x", (i&IPv6NibbleMaskHigh)>>IPv6NibbleShift))
|
|
}
|
|
|
|
return strings.Join(nibbles, ".") + ".ip6.arpa."
|
|
}
|