CLOUDFLARE: Add LOC support (#3857)

Fixes #2798.

I tested this locally and it seems to update the `LOC` record correctly.
This commit is contained in:
Kevin Ji 2025-12-01 06:12:10 -08:00 committed by GitHub
parent c073f2e654
commit ec9a9e23af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 141 additions and 60 deletions

View file

@ -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) | ❔ | ❔ | ❔ | ✅ | ❔ |

View file

@ -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
}

View file

@ -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

View file

@ -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