2023-10-02 17:44:47 +00:00
|
|
|
package pancheri
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha256"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
|
|
"net"
|
|
|
|
"os"
|
2023-10-02 17:48:31 +00:00
|
|
|
"strings"
|
2023-10-02 17:44:47 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type ZoneConfig struct {
|
|
|
|
Zone struct {
|
|
|
|
Root string `yaml:"root"`
|
|
|
|
Records []CfgRecord `yaml:"records"`
|
|
|
|
} `yaml:"zone"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type CfgRecord struct {
|
|
|
|
RecordType string `yaml:"type"`
|
|
|
|
Domains []string `yaml:"domains"`
|
|
|
|
Ipv4 net.IP `yaml:"ipv4"`
|
|
|
|
Ipv6 net.IP `yaml:"ipv6"`
|
|
|
|
Target string `yaml:"target"`
|
|
|
|
Content []string `yaml:"content"`
|
|
|
|
TTL uint `yaml:"ttl"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func LoadZone(path string) (*Zone, error) {
|
|
|
|
f, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var cfg ZoneConfig
|
|
|
|
decoder := yaml.NewDecoder(f)
|
|
|
|
|
|
|
|
err = decoder.Decode(&cfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = f.Close()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
reduced, err := yaml.Marshal(cfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// ReducedHash is a hash of the zonefile after it has been re-marshaled by libyaml.
|
|
|
|
// This is intended to prevent pointless changes of the serial when only minor formatting changes have been made
|
|
|
|
// ex. adding an extra space somewhere won't trigger a new serial
|
|
|
|
reducedHash := fmt.Sprintf("%x", sha256.Sum256(reduced))
|
|
|
|
// The serial is the first few bytes of this converted to an integer
|
|
|
|
|
|
|
|
// validate and convert
|
|
|
|
zone := Zone{
|
|
|
|
Root: cfg.Zone.Root,
|
|
|
|
ReducedHash: reducedHash,
|
|
|
|
Zonefile: path,
|
|
|
|
ARecords: nil,
|
|
|
|
AAAARecords: nil,
|
|
|
|
CNAMERecords: nil,
|
|
|
|
TXTRecords: nil,
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, record := range cfg.Zone.Records {
|
|
|
|
if record.RecordType == RuleTypeA {
|
|
|
|
// req.d fields: in, ip, ttl
|
|
|
|
if len(record.Domains) == 0 {
|
|
|
|
return nil, errors.New("A record must contain at least one domain")
|
|
|
|
}
|
|
|
|
if record.Ipv4 == nil {
|
|
|
|
return nil, errors.New("A record must contain ipv4 address")
|
|
|
|
}
|
|
|
|
if record.TTL == 0 {
|
|
|
|
return nil, errors.New("A record TTL cannot be 0 or empty")
|
|
|
|
}
|
|
|
|
for _, domain := range record.Domains {
|
2023-10-02 17:48:31 +00:00
|
|
|
if !strings.HasSuffix(domain, ".") {
|
|
|
|
domain = domain + "." + cfg.Zone.Root + "."
|
|
|
|
}
|
2023-10-02 17:44:47 +00:00
|
|
|
zone.ARecords = append(zone.ARecords, RecordA{
|
|
|
|
In: domain,
|
|
|
|
Ip: record.Ipv4.To4(),
|
|
|
|
TTL: record.TTL,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else if record.RecordType == RuleTypeAAAA {
|
|
|
|
// req.d fields: in, ip, ttl
|
|
|
|
if len(record.Domains) == 0 {
|
|
|
|
return nil, errors.New("AAAA record must contain at least one domain")
|
|
|
|
}
|
|
|
|
if record.Ipv6 == nil {
|
|
|
|
return nil, errors.New("AAAA record must contain ipv6 address")
|
|
|
|
}
|
|
|
|
if record.TTL == 0 {
|
|
|
|
return nil, errors.New("AAAA record TTL cannot be 0 or empty")
|
|
|
|
}
|
|
|
|
for _, domain := range record.Domains {
|
2023-10-02 17:48:31 +00:00
|
|
|
if !strings.HasSuffix(domain, ".") {
|
|
|
|
domain = domain + "." + cfg.Zone.Root + "."
|
|
|
|
}
|
2023-10-02 17:44:47 +00:00
|
|
|
zone.AAAARecords = append(zone.AAAARecords, RecordAAAA{
|
|
|
|
In: domain,
|
|
|
|
Ip: record.Ipv6,
|
|
|
|
TTL: record.TTL,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else if record.RecordType == RuleTypeCNAME {
|
|
|
|
// req.d fields: in, ip, ttl
|
|
|
|
if len(record.Domains) == 0 {
|
|
|
|
return nil, errors.New("CNAME record must contain at least one domain")
|
|
|
|
}
|
|
|
|
if len(record.Target) == 0 {
|
|
|
|
return nil, errors.New("CNAME record must contain target")
|
|
|
|
}
|
|
|
|
if record.TTL == 0 {
|
|
|
|
return nil, errors.New("CNAME record TTL cannot be 0 or empty")
|
|
|
|
}
|
|
|
|
for _, domain := range record.Domains {
|
2023-10-02 17:48:31 +00:00
|
|
|
if !strings.HasSuffix(domain, ".") {
|
|
|
|
domain = domain + "." + cfg.Zone.Root + "."
|
|
|
|
}
|
2023-10-02 17:44:47 +00:00
|
|
|
zone.CNAMERecords = append(zone.CNAMERecords, RecordCNAME{
|
|
|
|
In: domain,
|
|
|
|
Target: record.Target,
|
|
|
|
TTL: record.TTL,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else if record.RecordType == RuleTypeTXT {
|
|
|
|
// req.d fields: in, content, ttl
|
|
|
|
if len(record.Domains) == 0 {
|
|
|
|
return nil, errors.New("TXT record must contain at least one domain")
|
|
|
|
}
|
|
|
|
if len(record.Content) == 0 {
|
|
|
|
return nil, errors.New("TXT record must contain content")
|
|
|
|
}
|
|
|
|
if record.TTL == 0 {
|
|
|
|
return nil, errors.New("TXT record TTL cannot be 0 or empty")
|
|
|
|
}
|
|
|
|
for _, domain := range record.Domains {
|
2023-10-02 17:48:31 +00:00
|
|
|
if !strings.HasSuffix(domain, ".") {
|
|
|
|
domain = domain + "." + cfg.Zone.Root + "."
|
|
|
|
}
|
2023-10-02 17:44:47 +00:00
|
|
|
zone.TXTRecords = append(zone.TXTRecords, RecordTXT{
|
|
|
|
In: domain,
|
|
|
|
Content: record.Content,
|
|
|
|
TTL: record.TTL,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &zone, nil
|
|
|
|
}
|