From ec9a9e23af1ba6b878f6e60cba126750d462325d Mon Sep 17 00:00:00 2001 From: Kevin Ji <1146876+kevinji@users.noreply.github.com> Date: Mon, 1 Dec 2025 06:12:10 -0800 Subject: [PATCH] CLOUDFLARE: Add LOC support (#3857) Fixes #2798. I tested this locally and it seems to update the `LOC` record correctly. --- documentation/provider/index.md | 2 +- models/t_loc.go | 71 ++++++++++++++++++---- providers/cloudflare/cloudflareProvider.go | 58 +++++++++++------- providers/cloudflare/rest.go | 70 ++++++++++++++------- 4 files changed, 141 insertions(+), 60 deletions(-) diff --git a/documentation/provider/index.md b/documentation/provider/index.md index 1feac91a5..4b81545c8 100644 --- a/documentation/provider/index.md +++ b/documentation/provider/index.md @@ -158,7 +158,7 @@ Jump to a table: | [`AZURE_PRIVATE_DNS`](azure_private_dns.md) | ❌ | ❔ | ❌ | ✅ | ❔ | | [`BIND`](bind.md) | ❔ | ✅ | ✅ | ✅ | ✅ | | [`BUNNY_DNS`](bunny_dns.md) | ✅ | ❔ | ❌ | ✅ | ❌ | -| [`CLOUDFLAREAPI`](cloudflareapi.md) | ✅ | ❔ | ❌ | ✅ | ❔ | +| [`CLOUDFLAREAPI`](cloudflareapi.md) | ✅ | ❔ | ✅ | ✅ | ❔ | | [`CLOUDNS`](cloudns.md) | ✅ | ✅ | ✅ | ✅ | ❔ | | [`CNR`](cnr.md) | ✅ | ❌ | ❌ | ✅ | ❌ | | [`DESEC`](desec.md) | ❔ | ❔ | ❔ | ✅ | ❔ | diff --git a/models/t_loc.go b/models/t_loc.go index a8f1a7596..9ba1f8360 100644 --- a/models/t_loc.go +++ b/models/t_loc.go @@ -99,26 +99,20 @@ func (rc *RecordConfig) calculateLOCFields(d1 uint8, m1 uint8, s1 float32, ns st ) error { // Crazy hairy shit happens here. // We already got the useful "string" version earlier. ¯\_(ツ)_/¯ code golf... - const LOCEquator uint64 = 0x80000000 // 1 << 31 // RFC 1876, Section 2. - const LOCPrimeMeridian uint64 = 0x80000000 // 1 << 31 // RFC 1876, Section 2. - const LOCHours uint32 = 60 * 1000 - const LOCDegrees = 60 * LOCHours - const LOCAltitudeBase int32 = 100000 - - lat := uint64((uint32(d1) * LOCDegrees) + (uint32(m1) * LOCHours) + uint32(s1*1000)) - lon := uint64((uint32(d2) * LOCDegrees) + (uint32(m2) * LOCHours) + uint32(s2*1000)) + lat := uint32(d1)*dns.LOC_DEGREES + uint32(m1)*dns.LOC_HOURS + uint32(s1*1000) + lon := uint32(d2)*dns.LOC_DEGREES + uint32(m2)*dns.LOC_HOURS + uint32(s2*1000) if strings.ToUpper(ns) == "N" { - rc.LocLatitude = uint32(LOCEquator + lat) + rc.LocLatitude = dns.LOC_EQUATOR + lat } else { // "S" - rc.LocLatitude = uint32(LOCEquator - lat) + rc.LocLatitude = dns.LOC_EQUATOR - lat } if strings.ToUpper(ew) == "E" { - rc.LocLongitude = uint32(LOCPrimeMeridian + lon) + rc.LocLongitude = dns.LOC_PRIMEMERIDIAN + lon } else { // "W" - rc.LocLongitude = uint32(LOCPrimeMeridian - lon) + rc.LocLongitude = dns.LOC_PRIMEMERIDIAN - lon } // Altitude - altitude := (float64(al) + float64(LOCAltitudeBase)) * 100 + altitude := (float64(al) + dns.LOC_ALTITUDEBASE) * 100 clampedAltitude := math.Min(math.Max(0, altitude), float64(math.MaxUint32)) rc.LocAltitude = uint32(clampedAltitude) @@ -205,3 +199,54 @@ func getENotationInt(x float32) (uint8, error) { return packedValue, nil } + +func ReverseLatitude(lat uint32) (string, uint8, uint8, float64) { + var hemisphere string + if lat >= dns.LOC_EQUATOR { + hemisphere = "N" + lat = lat - dns.LOC_EQUATOR + } else { + hemisphere = "S" + lat = dns.LOC_EQUATOR - lat + } + degrees := uint8(lat / dns.LOC_DEGREES) + lat -= uint32(degrees) * dns.LOC_DEGREES + minutes := uint8(lat / dns.LOC_HOURS) + lat -= uint32(minutes) * dns.LOC_HOURS + seconds := float64(lat) / 1000 + + return hemisphere, degrees, minutes, seconds +} + +func ReverseLongitude(lon uint32) (string, uint8, uint8, float64) { + var hemisphere string + if lon >= dns.LOC_PRIMEMERIDIAN { + hemisphere = "E" + lon = lon - dns.LOC_PRIMEMERIDIAN + } else { + hemisphere = "W" + lon = dns.LOC_PRIMEMERIDIAN - lon + } + degrees := uint8(lon / dns.LOC_DEGREES) + lon -= uint32(degrees) * dns.LOC_DEGREES + minutes := uint8(lon / dns.LOC_HOURS) + lon -= uint32(minutes) * dns.LOC_HOURS + seconds := float64(lon) / 1000 + + return hemisphere, degrees, minutes, seconds +} + +func ReverseAltitude(packedAltitude uint32) float64 { + return float64(packedAltitude)/100 - 100000 +} + +// ReverseENotationInt produces a number from a mantissa_exponent 4bits:4bits uint8 +func ReverseENotationInt(packedValue uint8) float64 { + mantissa := float64((packedValue >> 4) & 0x0F) + exponent := int(packedValue & 0x0F) + + centimeters := mantissa * math.Pow10(exponent) + + // Return in meters + return centimeters / 100 +} diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index d3b4fd93c..cd2fbf9b9 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -53,7 +53,7 @@ var features = providers.DocumentationNotes{ providers.CanUseDS: providers.Can(), providers.CanUseDSForChildren: providers.Can(), providers.CanUseHTTPS: providers.Can(), - providers.CanUseLOC: providers.Cannot(), + providers.CanUseLOC: providers.Can(), providers.CanUseNAPTR: providers.Can(), providers.CanUsePTR: providers.Can(), providers.CanUseSRV: providers.Can(), @@ -708,28 +708,40 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS // Used on the "existing" records. type cfRecData struct { - Name string `json:"name"` - Target cfTarget `json:"target"` - Service string `json:"service"` // SRV - Proto string `json:"proto"` // SRV - Priority uint16 `json:"priority"` // SRV - Weight uint16 `json:"weight"` // SRV - Port uint16 `json:"port"` // SRV - Tag string `json:"tag"` // CAA - Flags uint16 `json:"flags"` // CAA/DNSKEY - Value string `json:"value"` // CAA - Usage uint8 `json:"usage"` // TLSA - Selector uint8 `json:"selector"` // TLSA - MatchingType uint8 `json:"matching_type"` // TLSA - Certificate string `json:"certificate"` // TLSA - Algorithm uint8 `json:"algorithm"` // SSHFP/DNSKEY/DS - HashType uint8 `json:"type"` // SSHFP - Fingerprint string `json:"fingerprint"` // SSHFP - Protocol uint8 `json:"protocol"` // DNSKEY - PublicKey string `json:"public_key"` // DNSKEY - KeyTag uint16 `json:"key_tag"` // DS - DigestType uint8 `json:"digest_type"` // DS - Digest string `json:"digest"` // DS + Name string `json:"name"` + Target cfTarget `json:"target"` + Service string `json:"service"` // SRV + Proto string `json:"proto"` // SRV + Priority uint16 `json:"priority"` // SRV + Weight uint16 `json:"weight"` // SRV + Port uint16 `json:"port"` // SRV + Tag string `json:"tag"` // CAA + Flags uint16 `json:"flags"` // CAA/DNSKEY + Value string `json:"value"` // CAA + Usage uint8 `json:"usage"` // TLSA + Selector uint8 `json:"selector"` // TLSA + MatchingType uint8 `json:"matching_type"` // TLSA + Certificate string `json:"certificate"` // TLSA + Algorithm uint8 `json:"algorithm"` // SSHFP/DNSKEY/DS + HashType uint8 `json:"type"` // SSHFP + Fingerprint string `json:"fingerprint"` // SSHFP + Protocol uint8 `json:"protocol"` // DNSKEY + PublicKey string `json:"public_key"` // DNSKEY + KeyTag uint16 `json:"key_tag"` // DS + DigestType uint8 `json:"digest_type"` // DS + Digest string `json:"digest"` // DS + Altitude float64 `json:"altitude"` // LOC + LatDegrees uint8 `json:"lat_degrees"` // LOC + LatDirection string `json:"lat_direction"` // LOC + LatMinutes uint8 `json:"lat_minutes"` // LOC + LatSeconds float64 `json:"lat_seconds"` // LOC + LongDegrees uint8 `json:"long_degrees"` // LOC + LongDirection string `json:"long_direction"` // LOC + LongMinutes uint8 `json:"long_minutes"` // LOC + LongSeconds float64 `json:"long_seconds"` // LOC + PrecisionHorz float64 `json:"precision_horz"` // LOC + PrecisionVert float64 `json:"precision_vert"` // LOC + Size float64 `json:"size"` // LOC } // cfTarget is a SRV target. A null target is represented by an empty string, but diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go index 9efd522ea..fdbc5d290 100644 --- a/providers/cloudflare/rest.go +++ b/providers/cloudflare/rest.go @@ -137,6 +137,26 @@ func cfSvcbData(rec *models.RecordConfig) *cfRecData { } } +func cfLocData(rec *models.RecordConfig) *cfRecData { + latDir, latDeg, latMin, latSec := models.ReverseLatitude(rec.LocLatitude) + longDir, longDeg, longMin, longSec := models.ReverseLongitude(rec.LocLongitude) + + return &cfRecData{ + Altitude: models.ReverseAltitude(rec.LocAltitude), + LatDegrees: latDeg, + LatDirection: latDir, + LatMinutes: latMin, + LatSeconds: latSec, + LongDegrees: longDeg, + LongDirection: longDir, + LongMinutes: longMin, + LongSeconds: longSec, + PrecisionHorz: models.ReverseENotationInt(rec.LocHorizPre), + PrecisionVert: models.ReverseENotationInt(rec.LocVertPre), + Size: models.ReverseENotationInt(rec.LocSize), + } +} + func cfNaptrData(rec *models.RecordConfig) *cfNaptrRecData { return &cfNaptrRecData{ Flags: rec.NaptrFlags, @@ -154,13 +174,12 @@ func (c *cloudflareProvider) createRecDiff2(rec *models.RecordConfig, domainID s content = rec.Metadata[metaOriginalIP] } prio := "" - if rec.Type == "MX" { + switch rec.Type { + case "MX": prio = fmt.Sprintf(" %d ", rec.MxPreference) - } - if rec.Type == "TXT" { + case "TXT": content = rec.GetTargetTXTJoined() - } - if rec.Type == "DS" { + case "DS": content = fmt.Sprintf("%d %d %d %s", rec.DsKeyTag, rec.DsAlgorithm, rec.DsDigestType, rec.DsDigest) } if msg == "" { @@ -179,28 +198,31 @@ func (c *cloudflareProvider) createRecDiff2(rec *models.RecordConfig, domainID s Content: content, Priority: &rec.MxPreference, } - if rec.Type == "SRV" { + switch rec.Type { + case "SRV": cf.Data = cfSrvData(rec) cf.Name = rec.GetLabelFQDN() - } else if rec.Type == "CAA" { + case "CAA": cf.Data = cfCaaData(rec) cf.Name = rec.GetLabelFQDN() cf.Content = "" - } else if rec.Type == "TLSA" { + case "TLSA": cf.Data = cfTlsaData(rec) cf.Name = rec.GetLabelFQDN() - } else if rec.Type == "SSHFP" { + case "SSHFP": cf.Data = cfSshfpData(rec) cf.Name = rec.GetLabelFQDN() - } else if rec.Type == "DNSKEY" { + case "DNSKEY": cf.Data = cfDnskeyData(rec) - } else if rec.Type == "DS" { + case "DS": cf.Data = cfDSData(rec) - } else if rec.Type == "NAPTR" { + case "NAPTR": cf.Data = cfNaptrData(rec) cf.Name = rec.GetLabelFQDN() - } else if rec.Type == "HTTPS" || rec.Type == "SVCB" { + case "HTTPS", "SVCB": cf.Data = cfSvcbData(rec) + case "LOC": + cf.Data = cfLocData(rec) } resp, err := c.cfClient.CreateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(domainID), cf) if err != nil { @@ -232,33 +254,35 @@ func (c *cloudflareProvider) modifyRecord(domainID, recID string, proxied bool, Priority: &rec.MxPreference, TTL: int(rec.TTL), } - if rec.Type == "TXT" { + switch rec.Type { + case "TXT": r.Content = rec.GetTargetTXTJoined() - } - if rec.Type == "SRV" { + case "SRV": r.Data = cfSrvData(rec) r.Name = rec.GetLabelFQDN() - } else if rec.Type == "CAA" { + case "CAA": r.Data = cfCaaData(rec) r.Name = rec.GetLabelFQDN() r.Content = "" - } else if rec.Type == "TLSA" { + case "TLSA": r.Data = cfTlsaData(rec) r.Name = rec.GetLabelFQDN() - } else if rec.Type == "SSHFP" { + case "SSHFP": r.Data = cfSshfpData(rec) r.Name = rec.GetLabelFQDN() - } else if rec.Type == "DNSKEY" { + case "DNSKEY": r.Data = cfDnskeyData(rec) r.Content = "" - } else if rec.Type == "DS" { + case "DS": r.Data = cfDSData(rec) r.Content = "" - } else if rec.Type == "NAPTR" { + case "NAPTR": r.Data = cfNaptrData(rec) r.Name = rec.GetLabelFQDN() - } else if rec.Type == "HTTPS" || rec.Type == "SVCB" { + case "HTTPS", "SVCB": r.Data = cfSvcbData(rec) + case "LOC": + r.Data = cfLocData(rec) } _, err := c.cfClient.UpdateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(domainID), r) return err