package pancheri import ( "crypto/sha256" "errors" "fmt" "gopkg.in/yaml.v2" "net" "os" "strings" ) 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 { if !strings.HasSuffix(domain, ".") { domain = domain + "." + cfg.Zone.Root + "." } 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 { if !strings.HasSuffix(domain, ".") { domain = domain + "." + cfg.Zone.Root + "." } 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 { if !strings.HasSuffix(domain, ".") { domain = domain + "." + cfg.Zone.Root + "." } 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 { if !strings.HasSuffix(domain, ".") { domain = domain + "." + cfg.Zone.Root + "." } zone.TXTRecords = append(zone.TXTRecords, RecordTXT{ In: domain, Content: record.Content, TTL: record.TTL, }) } } } return &zone, nil }