From 92ad51d0018505d26b4eb05428faf2f9eeff7a29 Mon Sep 17 00:00:00 2001 From: m5r Date: Mon, 22 Jul 2024 01:06:47 +0200 Subject: [PATCH] extract hardcoded records --- xip/records.go | 153 +++++++++++++++++++++++++++++++++++++++++++++++++ xip/xip.go | 138 +++++++++++--------------------------------- 2 files changed, 188 insertions(+), 103 deletions(-) create mode 100644 xip/records.go diff --git a/xip/records.go b/xip/records.go new file mode 100644 index 0000000..6fe7207 --- /dev/null +++ b/xip/records.go @@ -0,0 +1,153 @@ +package xip + +import ( + "fmt" + "net" + + "github.com/miekg/dns" + "local-ip.sh/utils" +) + +type hardcodedRecord struct { + A []net.IP // => dns.A + AAAA []net.IP // => dns.AAAA + TXT []string // => dns.TXT + MX []*dns.MX + CNAME []string // => dns.CNAME + SRV *dns.SRV +} + +var config = utils.GetConfig() +var hardcodedRecords = map[string]hardcodedRecord{ + // TODO: maybe --nameservers ns1.local-ip.sh.=137.66.40.11,ns2.local-ip.sh.=137.66.40.12 + fmt.Sprintf("ns.%s.", config.Domain): { + // record holding ip addresses of ns1 and ns2 + A: []net.IP{ + net.IPv4(137, 66, 40, 11), + net.IPv4(137, 66, 40, 12), + }, + }, + fmt.Sprintf("ns1.%s.", config.Domain): { + A: []net.IP{ + net.IPv4(137, 66, 40, 11), // fly.io edge-only ip address, see https://community.fly.io/t/custom-domains-certificate-is-stuck-on-awaiting-configuration/8329 + }, + }, + fmt.Sprintf("ns2.%s.", config.Domain): { + A: []net.IP{ + net.IPv4(137, 66, 40, 12), // fly.io edge-only ip address #2 + }, + }, + fmt.Sprintf("%s.", config.Domain): { + // same as ns.local-ip.sh, it's the same machine :) + A: []net.IP{ + net.IPv4(137, 66, 40, 11), + net.IPv4(137, 66, 40, 12), + }, + }, + fmt.Sprintf("_acme-challenge.%s.", config.Domain): { + // will be filled in later when requesting the wildcard certificate + TXT: []string{}, + }, +} + +// additional records I set up to host emails, feel free to change or remove them for your own needs +var extraRecords = map[string]hardcodedRecord{ + "local-ip.sh.": { + TXT: []string{"v=spf1 include:capsulecorp.dev ~all"}, + MX: []*dns.MX{ + {Preference: 10, Mx: "email.capsulecorp.dev."}, + }, + }, + "autodiscover.local-ip.sh.": { + CNAME: []string{ + "email.capsulecorp.dev.", + }, + }, + "_autodiscover._tcp.local-ip.sh.": { + SRV: &dns.SRV{ + Priority: 0, + Weight: 0, + Port: 443, + Target: "email.capsulecorp.dev.", + }, + }, + "autoconfig.local-ip.sh.": { + CNAME: []string{ + "email.capsulecorp.dev.", + }, + }, + "_dmarc.local-ip.sh.": { + TXT: []string{"v=DMARC1; p=none; rua=mailto:postmaster@local-ip.sh; ruf=mailto:admin@local-ip.sh"}, + }, + "dkim._domainkey.local-ip.sh.": { + TXT: []string{ + "v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMW6NFo34qzKRPbzK41GwbWncB8IDg1i2eA2VWznIVDmTzzsqILaBOGv2xokVpzZm0QRF9wSbeVUmvwEeQ7Z6wkfMjawenDEc3XxsNSvQUVBP6LU/xcm1zsR8wtD8r5J+Jm45pNFaateiM/kb/Eypp2ntdtd8CPsEgCEDpNb62LWdy0yzRdZ/M/fNn51UMN8hVFp4YfZngAt3bQwa6kPtgvTeqEbpNf5xanpDysNJt2S8zfqJMVGvnr8JaJiTv7ZlKMMp94aC5Ndcir1WbMyfmgSnGgemuCTVMWDGPJnXDi+8BQMH1b1hmTpWDiVdVlehyyWx5AfPrsWG9cEuDIfXwIDAQAB", + }, + }, +} +var records = mergeRecords(hardcodedRecords, extraRecords) + +func mergeRecords(a, b map[string]hardcodedRecord) map[string]hardcodedRecord { + result := make(map[string]hardcodedRecord) + for k, v := range a { + result[k] = v + } + for k, v := range b { + if r, ok := result[k]; ok { + result[k] = hardcodedRecord{ + A: uniqueIPs(append(r.A, v.A...)), + AAAA: uniqueIPs(append(r.AAAA, v.AAAA...)), + TXT: uniqueStrings(append(r.TXT, v.TXT...)), + MX: uniqueMX(append(r.MX, v.MX...)), + CNAME: uniqueStrings(append(r.CNAME, v.CNAME...)), + SRV: firstNonNil(r.SRV, v.SRV), + } + } else { + result[k] = v + } + } + return result +} + +func uniqueIPs(ips []net.IP) []net.IP { + seen := make(map[string]bool) + result := []net.IP{} + for _, ip := range ips { + if !seen[ip.String()] { + seen[ip.String()] = true + result = append(result, ip) + } + } + return result +} + +func uniqueStrings(strs []string) []string { + seen := make(map[string]bool) + result := []string{} + for _, str := range strs { + if !seen[str] { + seen[str] = true + result = append(result, str) + } + } + return result +} + +func uniqueMX(mxs []*dns.MX) []*dns.MX { + seen := make(map[string]uint16) + result := []*dns.MX{} + for _, mx := range mxs { + if pref, exists := seen[mx.Mx]; !exists || pref > mx.Preference { + seen[mx.Mx] = mx.Preference + result = append(result, mx) + } + } + return result +} + +func firstNonNil[T any](a, b *T) *T { + if a != nil { + return a + } + return b +} diff --git a/xip/xip.go b/xip/xip.go index 3099f42..531ec64 100644 --- a/xip/xip.go +++ b/xip/xip.go @@ -17,113 +17,45 @@ type Xip struct { nameServers []*dns.NS } -type HardcodedRecord struct { - A []net.IP // => dns.A - AAAA []net.IP // => dns.AAAA - TXT []string // => dns.TXT - MX []*dns.MX - CNAME []string // => dns.CNAME - SRV *dns.SRV -} - var ( - flyRegion = os.Getenv("FLY_REGION") - dottedIpV4Regex = regexp.MustCompile(`(?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})($|[.-])`) - dashedIpV4Regex = regexp.MustCompile(`(?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\-?\b){4})($|[.-])`) - hardcodedRecords = map[string]HardcodedRecord{ - // TODO: maybe --nameservers ns1.local-ip.sh.=137.66.40.11,ns2.local-ip.sh.=137.66.40.12 - "ns.local-ip.sh.": { - // record holding ip addresses of ns1 and ns2 - A: []net.IP{ - net.IPv4(137, 66, 40, 11), - net.IPv4(137, 66, 40, 12), - }, - }, - "ns1.local-ip.sh.": { - A: []net.IP{ - net.IPv4(137, 66, 40, 11), // fly.io edge-only ip address, see https://community.fly.io/t/custom-domains-certificate-is-stuck-on-awaiting-configuration/8329 - }, - }, - "ns2.local-ip.sh.": { - A: []net.IP{ - net.IPv4(137, 66, 40, 12), // fly.io edge-only ip address #2 - }, - }, - "local-ip.sh.": { - A: []net.IP{ - net.IPv4(137, 66, 40, 11), // fly.io edge-only ip address - }, - TXT: []string{"v=spf1 include:capsulecorp.dev ~all"}, - MX: []*dns.MX{ - {Preference: 10, Mx: "email.capsulecorp.dev."}, - }, - }, - "autodiscover.local-ip.sh.": { - CNAME: []string{ - "email.capsulecorp.dev.", - }, - }, - "_autodiscover._tcp.local-ip.sh.": { - SRV: &dns.SRV{ - Priority: 0, - Weight: 0, - Port: 443, - Target: "email.capsulecorp.dev.", - }, - }, - "autoconfig.local-ip.sh.": { - CNAME: []string{ - "email.capsulecorp.dev.", - }, - }, - "_dmarc.local-ip.sh.": { - TXT: []string{"v=DMARC1; p=none; rua=mailto:postmaster@local-ip.sh; ruf=mailto:admin@local-ip.sh"}, - }, - "dkim._domainkey.local-ip.sh.": { - TXT: []string{ - "v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMW6NFo34qzKRPbzK41GwbWncB8IDg1i2eA2VWznIVDmTzzsqILaBOGv2xokVpzZm0QRF9wSbeVUmvwEeQ7Z6wkfMjawenDEc3XxsNSvQUVBP6LU/xcm1zsR8wtD8r5J+Jm45pNFaateiM/kb/Eypp2ntdtd8CPsEgCEDpNb62LWdy0yzRdZ/M/fNn51UMN8hVFp4YfZngAt3bQwa6kPtgvTeqEbpNf5xanpDysNJt2S8zfqJMVGvnr8JaJiTv7ZlKMMp94aC5Ndcir1WbMyfmgSnGgemuCTVMWDGPJnXDi+8BQMH1b1hmTpWDiVdVlehyyWx5AfPrsWG9cEuDIfXwIDAQAB", - }, - }, - "_acme-challenge.local-ip.sh.": { - // will be filled in later when requesting the wildcard certificate - TXT: []string{}, - }, - } + flyRegion = os.Getenv("FLY_REGION") + dottedIpV4Regex = regexp.MustCompile(`(?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})($|[.-])`) + dashedIpV4Regex = regexp.MustCompile(`(?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\-?\b){4})($|[.-])`) ) func (xip *Xip) SetTXTRecord(fqdn string, value string) { utils.Logger.Debug().Str("fqdn", fqdn).Str("value", value).Msg("Trying to set TXT record") - if fqdn != "_acme-challenge.local-ip.sh." { + if fqdn != fmt.Sprintf("_acme-challenge.%s.", config.Domain) { utils.Logger.Debug().Msg("Not allowed, abort") return } - if records, ok := hardcodedRecords[fqdn]; ok { - records.TXT = []string{value} - hardcodedRecords["_acme-challenge.local-ip.sh."] = records + if rootRecords, ok := records[fqdn]; ok { + rootRecords.TXT = []string{value} + records[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = rootRecords } } func (xip *Xip) UnsetTXTRecord(fqdn string) { utils.Logger.Debug().Str("fqdn", fqdn).Msg("Trying to set TXT record") - if fqdn != "_acme-challenge.local-ip.sh." { + if fqdn != fmt.Sprintf("_acme-challenge.%s.", config.Domain) { utils.Logger.Debug().Msg("Not allowed, abort") return } - if records, ok := hardcodedRecords[fqdn]; ok { - records.TXT = []string{} - hardcodedRecords["_acme-challenge.local-ip.sh."] = records + if rootRecords, ok := records[fqdn]; ok { + rootRecords.TXT = []string{} + records[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = rootRecords } } func (xip *Xip) fqdnToA(fqdn string) []*dns.A { normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].A != nil { - var records []*dns.A + if records[normalizedFqdn].A != nil { + var aRecords []*dns.A - for _, record := range hardcodedRecords[normalizedFqdn].A { - records = append(records, &dns.A{ + for _, record := range records[normalizedFqdn].A { + aRecords = append(aRecords, &dns.A{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), Name: fqdn, @@ -134,7 +66,7 @@ func (xip *Xip) fqdnToA(fqdn string) []*dns.A { }) } - return records + return aRecords } for _, ipV4RE := range []*regexp.Regexp{dashedIpV4Regex, dottedIpV4Regex} { @@ -167,15 +99,15 @@ func (xip *Xip) answerWithAuthority(question dns.Question, message *dns.Msg) { func (xip *Xip) handleA(question dns.Question, message *dns.Msg) { fqdn := question.Name - records := xip.fqdnToA(fqdn) + aRecords := xip.fqdnToA(fqdn) - if len(records) == 0 { + if len(aRecords) == 0 { message.Rcode = dns.RcodeNameError xip.answerWithAuthority(question, message) return } - for _, record := range records { + for _, record := range aRecords { message.Answer = append(message.Answer, record) } } @@ -183,12 +115,12 @@ func (xip *Xip) handleA(question dns.Question, message *dns.Msg) { func (xip *Xip) handleAAAA(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].AAAA == nil { + if records[normalizedFqdn].AAAA == nil { xip.answerWithAuthority(question, message) return } - for _, record := range hardcodedRecords[normalizedFqdn].AAAA { + for _, record := range records[normalizedFqdn].AAAA { message.Answer = append(message.Answer, &dns.AAAA{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -238,12 +170,12 @@ func chunkBy(str string, chunkSize int) (chunks []string) { func (xip *Xip) handleTXT(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].TXT == nil { + if records[normalizedFqdn].TXT == nil { xip.answerWithAuthority(question, message) return } - for _, record := range hardcodedRecords[normalizedFqdn].TXT { + for _, record := range records[normalizedFqdn].TXT { message.Answer = append(message.Answer, &dns.TXT{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -259,12 +191,12 @@ func (xip *Xip) handleTXT(question dns.Question, message *dns.Msg) { func (xip *Xip) handleMX(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].MX == nil { + if records[normalizedFqdn].MX == nil { xip.answerWithAuthority(question, message) return } - for _, record := range hardcodedRecords[normalizedFqdn].MX { + for _, record := range records[normalizedFqdn].MX { message.Answer = append(message.Answer, &dns.MX{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -281,12 +213,12 @@ func (xip *Xip) handleMX(question dns.Question, message *dns.Msg) { func (xip *Xip) handleCNAME(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].CNAME == nil { + if records[normalizedFqdn].CNAME == nil { xip.answerWithAuthority(question, message) return } - for _, record := range hardcodedRecords[normalizedFqdn].CNAME { + for _, record := range records[normalizedFqdn].CNAME { message.Answer = append(message.Answer, &dns.CNAME{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -302,7 +234,7 @@ func (xip *Xip) handleCNAME(question dns.Question, message *dns.Msg) { func (xip *Xip) handleSRV(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].SRV == nil { + if records[normalizedFqdn].SRV == nil { xip.answerWithAuthority(question, message) return } @@ -314,10 +246,10 @@ func (xip *Xip) handleSRV(question dns.Question, message *dns.Msg) { Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, - Priority: hardcodedRecords[normalizedFqdn].SRV.Priority, - Weight: hardcodedRecords[normalizedFqdn].SRV.Weight, - Port: hardcodedRecords[normalizedFqdn].SRV.Port, - Target: hardcodedRecords[normalizedFqdn].SRV.Target, + Priority: records[normalizedFqdn].SRV.Priority, + Weight: records[normalizedFqdn].SRV.Weight, + Port: records[normalizedFqdn].SRV.Port, + Target: records[normalizedFqdn].SRV.Target, }) } @@ -334,9 +266,9 @@ func (xip *Xip) soaRecord(question dns.Question) *dns.SOA { Ttl: uint32((time.Minute * 5).Seconds()), Rdlength: 0, } - soa.Ns = "ns1.local-ip.sh." - soa.Mbox = "admin.local-ip.sh." - soa.Serial = 2022102800 + soa.Ns = xip.nameServers[0].Ns + soa.Mbox = config.Email + soa.Serial = 2024072200 soa.Refresh = uint32((time.Minute * 15).Seconds()) soa.Retry = uint32((time.Minute * 15).Seconds()) soa.Expire = uint32((time.Minute * 30).Seconds())