1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
|
// Package [libdns] defines core interfaces that should be implemented by
// packages that interact with DNS provider clients. These interfaces are
// small and idiomatic Go interfaces with well-defined semantics for the
// purposes of reading and manipulating DNS records using DNS provider APIs.
//
// This documentation uses the definitions for terms from RFC 9499:
// https://datatracker.ietf.org/doc/html/rfc9499
//
// This package represents records with the [Record] interface, which is any
// type that can transform itself into the [RR] struct. This interface is
// implemented by the various record abstractions this package offers: [RR]
// structs, where the data is serialized as a single opaque string as if in
// a zone file, being a type-agnostic [Resource Record] (that is, a name,
// type, class, TTL, and data); and individual RR-type structures, where the
// data is parsed into its separate fields for easier manipulation by Go
// programs (for example: [SRV], [TXT], and [ServiceBinding] types). This
// hybrid design grants great flexibility for both DNS provider packages and
// consumer Go programs.
//
// [Record] values should not be primitvely compared (==) unless they are [RR],
// because other struct types contain maps, for which equality is not defined;
// additionally, some packages may attach custom data to each RR struct-type's
// `ProviderData` field, whose values might not be comparable either. The
// `ProviderData` field is not portable across providers, or possibly even
// zones. Because it is not portable, and we want to ensure that [RR] structs
// remain both portable and comparable, the `RR()` method does not preserve
// `ProviderData` in its return value. Users of libdns packages should check
// the documentation of provider packages, as some may use the `ProviderData`
// field to reduce API calls / increase effiency. But implementations must
// never rely on `ProviderData` for correctness if possible (and should
// document clearly otherwise).
//
// Implementations of the libdns interfaces should accept as input any [Record]
// value, and should return as output the concrete struct types that implement
// the [Record] interface (i.e. [Address], [TXT], [ServiceBinding], etc). This
// is important to ensure the provider libraries are robust and also predictable:
// callers can reliably type-switch on the output to immediately access structured
// data about each record without the possibility of errors. Returned values should
// be of types defined by this package to make type-assertions reliable.
//
// Records are described independently of any particular zone, a convention that
// grants records portability across zones. As such, record names are partially
// qualified, i.e. relative to the zone. For example, a record called “sub” in
// zone “example.com.” represents a fully-qualified domain name (FQDN) of
// “sub.example.com.”. Implementations should expect that input records conform
// to this standard, while also ensuring that output records do; adjustments to
// record names may need to be made before or after provider API calls, for example,
// to maintain consistency with all other [libdns] packages. Helper functions are
// available in this package to convert between relative and absolute names;
// see [RelativeName] and [AbsoluteName].
//
// Although zone names are a required input, [libdns] does not coerce any
// particular representation of DNS zones; only records. Since zone name and
// records are separate inputs in [libdns] interfaces, it is up to the caller to
// maintain the pairing between a zone's name and its records.
//
// All interface implementations must be safe for concurrent/parallel use,
// meaning 1) no data races, and 2) simultaneous method calls must result in
// either both their expected outcomes or an error. For example, if
// [libdns.RecordAppender.AppendRecords] is called simultaneously, and two API
// requests are made to the provider at the same time, the result of both requests
// must be visible after they both complete; if the provider does not synchronize
// the writing of the zone file and one request overwrites the other, then the
// client implementation must take care to synchronize on behalf of the incompetent
// provider. This synchronization need not be global; for example: the scope of
// synchronization might only need to be within the same zone, allowing multiple
// requests at once as long as all of them are for different zone. (Exact logic
// depends on the provider.)
//
// Some service providers APIs may enforce rate limits or have sporadic errors.
// It is generally expected that libdns provider packages implement basic retry
// logic (e.g. retry up to 3-5 times with backoff in the event of a connection error
// or some HTTP error that may be recoverable, including 5xx or 429s) when it is
// safe to do so. Retrying/recovering from errors should not add substantial latency,
// though. If it will take longer than a couple seconds, best to return an error.
//
// [Resource Record]: https://en.wikipedia.org/wiki/Domain_Name_System#Resource_records
package libdns
import (
"context"
"strings"
)
// [RecordGetter] can get records from a DNS zone.
type RecordGetter interface {
// GetRecords returns all the records in the DNS zone.
//
// DNSSEC-related records are typically not included in the output, but this
// behavior is implementation-defined. If an implementation includes DNSSEC
// records in the output, this behavior should be documented.
//
// Implementations must honor context cancellation and be safe for concurrent
// use.
GetRecords(ctx context.Context, zone string) ([]Record, error)
}
// [RecordAppender] can non-destructively add new records to a DNS zone.
type RecordAppender interface {
// AppendRecords creates the inputted records in the given zone and returns
// the populated records that were created. It never changes existing records.
//
// Therefore, it makes little sense to use this method with CNAME-type
// records since if there are no existing records with the same name, it
// behaves the same as [libdns.RecordSetter.SetRecords], and if there are
// existing records with the same name, it will either fail or leave the
// zone in an invalid state.
//
// Implementations should return struct types defined by this package which
// correspond with the specific RR-type (instead of the opaque [RR] struct).
//
// Implementations must honor context cancellation and be safe for concurrent
// use.
AppendRecords(ctx context.Context, zone string, recs []Record) ([]Record, error)
}
// [RecordSetter] can set new or update existing records in a DNS zone.
type RecordSetter interface {
// SetRecords updates the zone so that the records described in the input are
// reflected in the output. It may create or update records or—depending on
// the record type—delete records to maintain parity with the input. No other
// records are affected. It returns the records which were set.
//
// For any (name, type) pair in the input, SetRecords ensures that the only
// records in the output zone with that (name, type) pair are those that were
// provided in the input.
//
// In RFC 9499 terms, SetRecords appends, modifies, or deletes records in the
// zone so that for each RRset in the input, the records provided in the input
// are the only members of their RRset in the output zone.
//
// SetRecords is distinct from [libdns.RecordAppender.AppendRecords] in that
// AppendRecords *only* adds records to the zone, while SetRecords may also
// delete records if necessary. Therefore, SetRecords behaves similarly to
// the following code:
//
// func SetRecords(ctx context.Context, zone string, recs []Record) ([]Record, error) {
// prevs, _ := p.GetRecords(ctx, zone)
// toDelete := []Record{}
// for _, prev := range prevs {
// for _, new := range recs {
// if prev.RR().Name == new.RR().Name && prev.RR().Type == new.RR().Type {
// toDelete = append(toDelete, prev)
// }
// }
// }
// DeleteRecords(ctx, zone, toDelete)
// return AppendRecords(ctx, zone, recs)
// }
//
// Implementations may decide whether or not to support DNSSEC-related records
// in calls to SetRecords, but should document their decision. Note that the
// decision to support DNSSEC records in SetRecords is independent of the
// decision to support them in [libdns.RecordGetter.GetRecords], so callers
// should not blindly call SetRecords with the output of
// [libdns.RecordGetter.GetRecords].
//
// If possible, implementations should make SetRecords atomic, such that if
// err == nil, then all of the requested changes were made, and if err != nil,
// then the zone remains as if the method was never called. However, as very
// few providers offer batch/atomic operations, the actual result of a call
// where err != nil is undefined. Implementations may implement synthetic
// atomicity that rolls back partial changes on failure ONLY if it can be
// done reliably. For calls that error atomically, implementations should
// return [AtomicErr] as the error so callers may know that their zone remains
// in a consistent state. Implementations should document their atomicity
// guarantees (or lack thereof).
//
// If SetRecords is used to add a CNAME record to a name with other existing
// non-DNSSEC records, implementations may either fail with an error, add
// the CNAME and leave the other records in place (in violation of the DNS
// standards), or add the CNAME and remove the other preexisting records.
// Therefore, users should proceed with caution when using SetRecords with
// CNAME records.
//
// Implementations should return struct types defined by this package which
// correspond with the specific RR-type (instead of the opaque [RR] struct).
//
// Implementations must honor context cancellation and be safe for concurrent
// use.
//
// # Examples
//
// Example 1:
//
// ;; Original zone
// example.com. 3600 IN A 192.0.2.1
// example.com. 3600 IN A 192.0.2.2
// example.com. 3600 IN TXT "hello world"
//
// ;; Input
// example.com. 3600 IN A 192.0.2.3
//
// ;; Resultant zone
// example.com. 3600 IN A 192.0.2.3
// example.com. 3600 IN TXT "hello world"
//
// Example 2:
//
// ;; Original zone
// alpha.example.com. 3600 IN AAAA 2001:db8::1
// alpha.example.com. 3600 IN AAAA 2001:db8::2
// beta.example.com. 3600 IN AAAA 2001:db8::3
// beta.example.com. 3600 IN AAAA 2001:db8::4
//
// ;; Input
// alpha.example.com. 3600 IN AAAA 2001:db8::1
// alpha.example.com. 3600 IN AAAA 2001:db8::2
// alpha.example.com. 3600 IN AAAA 2001:db8::5
//
// ;; Resultant zone
// alpha.example.com. 3600 IN AAAA 2001:db8::1
// alpha.example.com. 3600 IN AAAA 2001:db8::2
// alpha.example.com. 3600 IN AAAA 2001:db8::5
// beta.example.com. 3600 IN AAAA 2001:db8::3
// beta.example.com. 3600 IN AAAA 2001:db8::4
SetRecords(ctx context.Context, zone string, recs []Record) ([]Record, error)
}
// [RecordDeleter] can delete records from a DNS zone.
type RecordDeleter interface {
// DeleteRecords deletes the given records from the zone if they exist in the
// zone and exactly match the input. If the input records do not exist in the
// zone, they are silently ignored. DeleteRecords returns only the the records
// that were deleted, and does not return any records that were provided in the
// input but did not exist in the zone.
//
// DeleteRecords only deletes records from the zone that *exactly* match the
// input records—that is, the name, type, TTL, and value all must be identical
// to a record in the zone for it to be deleted.
//
// As a special case, you may leave any of the fields [libdns.Record.Type],
// [libdns.Record.TTL], or [libdns.Record.Value] empty ("", 0, and ""
// respectively). In this case, DeleteRecords will delete any records that
// match the other fields, regardless of the value of the fields that were left
// empty. Note that this behavior does *not* apply to the [libdns.Record.Name]
// field, which must always be specified.
//
// Note that it is semantically invalid to remove the last “NS” record from a
// zone, so attempting to do is undefined behavior.
//
// Implementations should return struct types defined by this package which
// correspond with the specific RR-type (instead of the opaque [RR] struct).
//
// Implementations must honor context cancellation and be safe for concurrent
// use.
DeleteRecords(ctx context.Context, zone string, recs []Record) ([]Record, error)
}
// [ZoneLister] can list available DNS zones.
type ZoneLister interface {
// ListZones returns the list of available DNS zones for use by other
// [libdns] methods. Not every upstream provider API supports listing
// available zones, and very few [libdns]-dependent packages use this
// method, so this method is optional.
//
// Implementations must honor context cancellation and be safe for
// concurrent use.
ListZones(ctx context.Context) ([]Zone, error)
}
// [Zone] is a generalized representation of a DNS zone.
type Zone struct {
Name string
}
// [RelativeName] makes “fqdn” relative to “zone”. For example, for a FQDN of
// “sub.example.com” and a zone of “example.com.”, it returns “sub”.
//
// If fqdn is the same as zone (and both are non-empty), “@” is returned.
//
// If fqdn cannot be expressed relative to zone, the input fqdn is
// returned.
func RelativeName(fqdn, zone string) string {
// liberally ignore trailing dots on both fqdn and zone, because
// the relative name won't have a trailing dot anyway; I assume
// this won't be problematic...?
// (initially implemented because Cloudflare returns "fully-
// qualified" domains in their records without a trailing dot,
// but the input zone typically has a trailing dot)
rel := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(fqdn, "."), strings.TrimSuffix(zone, ".")), ".")
if rel == "" && fqdn != "" && zone != "" {
return "@"
}
return rel
}
// [AbsoluteName] makes name into a fully-qualified domain name (FQDN) by
// prepending it to zone and tidying up the dots. For example, an input of
// name “sub” and zone “example.com.” will return “sub.example.com.”. If
// the name ends with a dot, it will be returned as the FQDN.
//
// Using “@” as the name is the recommended way to represent the root of the
// zone; however, unlike the [Record] struct, using the empty string "" for the
// name *is* permitted here, and will be treated identically to “@”.
//
// In the name already has a trailing dot, it is returned as-is. This is similar
// to the behavior of [path/filepath.Abs], and means that [AbsoluteName] is
// idempotent, so it is safe to call multiple times without first checking if
// the name is absolute or relative.
func AbsoluteName(name, zone string) string {
if zone == "" {
return strings.Trim(name, ".")
}
if name == "" || name == "@" {
return zone
}
if strings.HasSuffix(name, ".") {
// Already a FQDN, so just return it
return name
}
return name + "." + zone
}
// AtomicErr should be returned as the error when a method errors
// atomically. When this error type is returned, the caller can
// know that their zone remains in a consistent state despite an
// error.
type AtomicErr error
|