eurodns

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 13 Imported by: 0

README

eurodns-go

Go client for the EuroDNS User REST API (https://rest-api.eurodns.com, spec at https://docapi.eurodns.com), focused on DNS zone management.

Status: pre-alpha. Not yet tested against the live EuroDNS API.

Packages

Package Content
eurodns (module root) Client (zones, records, check, snapshots), shared rate-limited Transport, typed errors. Talks to EuroDNS directly (X-APP-ID/X-API-KEY) or to an eurodns-gate (bearer token).
rrset Groups records into RRsets (host + type), host normalization and glob matching, content hash.
zonediff RRset-level delta between two zones, and applying changes while keeping every other record byte-identical.
eurodnstest In-memory fake of the DNS zone API for tests (ids, forwards, check, snapshots, rate limiting).

Design points:

  • Rate limit: EuroDNS allows 10 requests per 10 seconds per source IP and bans progressively. The Transport waits instead of dropping (default one request per 1.1 s) and retries on 429/5xx. Share one Transport between all clients of a process.
  • Lossless round-trip: unknown fields and URL/mail forwards are preserved, so a zone read then saved is unchanged except for your edits.
  • If-Match: sent on writes when requested. EuroDNS ignores it; eurodns-gate enforces it.

Usage

c, err := eurodns.New(
	eurodns.WithAuth(eurodns.APIKeyAuth{AppID: appID, APIKey: apiKey}),
)
if err != nil {
	return err
}

z, err := c.GetZone(ctx, "example.com")
if err != nil {
	return err
}
z.Records = append(z.Records, eurodns.Record{Type: "TXT", Host: "_demo", TTL: 600, RData: `"hello"`})
if err := c.SaveZone(ctx, "example.com", z, eurodns.WithIfMatch(z.ETag)); err != nil {
	return err
}

Through a gate:

c, err := eurodns.New(
	eurodns.WithBaseURL("https://eurodns-gate.example.com"),
	eurodns.WithAuth(eurodns.BearerAuth{Source: myTokenSource}),
)

Note: PUT /dns-zones/{domain} saves the whole zone. Always start from a fresh GetZone, never from a partial zone.

License

MIT — see LICENSE.

Documentation

Overview

Package eurodns is a client for the EuroDNS User REST API (https://docapi.eurodns.com), focused on DNS zone management.

The same client talks either to EuroDNS directly (X-APP-ID / X-API-KEY) or to an eurodns-gate instance (bearer token), through the Authenticator option. All requests go through a shared Transport that enforces the EuroDNS rate limit (10 requests per 10 seconds per source IP).

Index

Constants

View Source
const (
	FeatureIfMatch   = "if-match"
	FeatureOwnership = "ownership"
	FeatureAdopt     = "adopt"
)

Gate feature names.

View Source
const DefaultBaseURL = "https://rest-api.eurodns.com"

DefaultBaseURL is the EuroDNS REST API endpoint.

View Source
const GateInfoPath = "/.well-known/eurodns-gate"

GateInfoPath is the extension endpoint served by eurodns-gate only.

Variables

View Source
var AllowedTTLs = []int{600, 900, 1800, 3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800}

AllowedTTLs is the fixed list of TTL values accepted by EuroDNS.

View Source
var ErrNotGate = errors.New("eurodns: endpoint is not an eurodns-gate")

ErrNotGate is returned by GateInfo when the endpoint is plain EuroDNS.

Functions

func IsForbidden

func IsForbidden(err error) bool

func IsNotFound

func IsNotFound(err error) bool

func IsPreconditionFailed

func IsPreconditionFailed(err error) bool

func IsRateLimited

func IsRateLimited(err error) bool

Types

type APIError

type APIError struct {
	StatusCode int
	Method     string
	Path       string
	Body       []byte
	Report     *ValidationReport // parsed when the body is a zone with a report
	Reason     string            // eurodns-gate error code, e.g. "foreign_rrset", "not_owner", "tainted"
	Message    string            // human-readable message (gate "message" or EuroDNS "title")
	Code       string            // EuroDNS error code, e.g. "FORBIDDEN_ACTION", "INVALID_API_KEY"
	Errors     []ErrorItem       // EuroDNS error list, as returned
}

APIError is returned for non-2xx responses.

func (*APIError) Error

func (e *APIError) Error() string

type APIKeyAuth

type APIKeyAuth struct {
	AppID  string
	APIKey string
}

APIKeyAuth authenticates directly against EuroDNS.

func (APIKeyAuth) Apply

func (a APIKeyAuth) Apply(_ context.Context, req *http.Request) error

Apply sets X-APP-ID and X-API-KEY and removes any bearer token.

type AdoptRRset

type AdoptRRset struct {
	Host string `json:"host"`
	Type string `json:"type"`
}

AdoptRRset identifies one RRset to adopt.

type AdoptRequest

type AdoptRequest struct {
	RRsets []AdoptRRset `json:"rrsets"`
	Owner  string       `json:"owner,omitempty"` // owner team; defaults to the caller's team
}

AdoptRequest is the body of POST /.well-known/eurodns-gate/zones/{domain}/adopt.

type Authenticator

type Authenticator interface {
	Apply(ctx context.Context, req *http.Request) error
}

Authenticator sets credentials on an outgoing request.

type BearerAuth

type BearerAuth struct {
	Source TokenSource
}

BearerAuth authenticates against eurodns-gate with a bearer token.

func (BearerAuth) Apply

func (a BearerAuth) Apply(ctx context.Context, req *http.Request) error

Apply sets Authorization: Bearer and removes any EuroDNS key headers.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a EuroDNS (or eurodns-gate) API client. Safe for concurrent use.

func New

func New(opts ...Option) (*Client, error)

New builds a Client.

func (*Client) AddRecords

func (c *Client) AddRecords(ctx context.Context, domain string, recs []Record, opts ...WriteOption) (*Zone, error)

AddRecords adds records (POST /dns-zones/{domain}/dns-records).

func (*Client) Adopt

func (c *Client) Adopt(ctx context.Context, domain string, req AdoptRequest) error

Adopt asks eurodns-gate to take ownership of existing foreign RRsets.

func (*Client) CheckZone

func (c *Client) CheckZone(ctx context.Context, domain string, z *Zone) (*Zone, error)

CheckZone validates a zone without saving it (POST /dns-zones/{domain}/check).

func (*Client) DeleteRecord

func (c *Client) DeleteRecord(ctx context.Context, domain string, id int64, opts ...WriteOption) (*Zone, error)

DeleteRecord deletes one record by id (DELETE /dns-zones/{domain}/dns-records/{id}).

func (*Client) GateInfo

func (c *Client) GateInfo(ctx context.Context) (*GateInfo, error)

GateInfo queries GateInfoPath. Returns ErrNotGate on 404 (plain EuroDNS). 401, 403 and 405 are also treated as "not a gate": the gate serves this endpoint without authentication.

func (*Client) GetZone

func (c *Client) GetZone(ctx context.Context, domain string) (*Zone, error)

GetZone returns the zone with its records and forwards. ETag is filled when the server returns one.

func (*Client) ListSnapshots

func (c *Client) ListSnapshots(ctx context.Context, domain string) ([]Snapshot, error)

ListSnapshots lists zone snapshots.

func (*Client) SaveZone

func (c *Client) SaveZone(ctx context.Context, domain string, z *Zone, opts ...WriteOption) error

SaveZone replaces the whole zone (PUT /dns-zones/{domain}).

func (*Client) Transport

func (c *Client) Transport() *Transport

Transport returns the client's transport, to share it.

type ErrorItem

type ErrorItem struct {
	Status string          `json:"status"`
	Code   string          `json:"code"`
	Title  string          `json:"title"`
	Meta   json.RawMessage `json:"meta,omitempty"`
}

ErrorItem is one entry of the EuroDNS error body {"errors":[{"status":"403","code":"FORBIDDEN_ACTION","title":"...","meta":null}]}.

type GateInfo

type GateInfo struct {
	Version  string   `json:"version"`
	Features []string `json:"features"` // e.g. "if-match", "ownership", "adopt"
}

GateInfo is returned by an eurodns-gate on GET /.well-known/eurodns-gate.

func (*GateInfo) HasFeature

func (g *GateInfo) HasFeature(name string) bool

HasFeature reports whether the gate advertises a feature.

type Option

type Option func(*options)

Option configures a Client.

func WithAuth

func WithAuth(a Authenticator) Option

func WithBaseURL

func WithBaseURL(u string) Option

func WithHTTPClient

func WithHTTPClient(c *http.Client) Option

func WithMaxRetries

func WithMaxRetries(n int) Option

func WithRateLimit

func WithRateLimit(every time.Duration, burst int) Option

WithRateLimit sets the client-side limiter: one request every `every`, with the given burst. Default: one request per 1.1s, burst 5.

func WithRetryBackoff

func WithRetryBackoff(base, max time.Duration) Option

WithRetryBackoff sets the retry backoff (exponential with jitter, capped). Defaults to 10s / 2m: EuroDNS extends its ban on every violation during a ban, so retries must wait at least one rate window.

func WithTransport

func WithTransport(t *Transport) Option

WithTransport shares an existing Transport (and therefore its rate limiter) between several clients or with a reverse proxy.

func WithUserAgent

func WithUserAgent(ua string) Option

type Problem

type Problem struct {
	Severity Severity
	Message  string
	Host     string
	Type     string
}

Problem is a flattened, readable validation problem.

func (Problem) String

func (p Problem) String() string

type Record

type Record struct {
	ID       *int64                     `json:"id,omitempty"`
	Type     string                     `json:"type"`
	Host     string                     `json:"host"`
	TTL      int                        `json:"ttl"`
	RData    string                     `json:"rdata"`
	Updated  *bool                      `json:"updated,omitempty"`
	Locked   *bool                      `json:"locked,omitempty"`
	IsDynDNS *bool                      `json:"isDynDns,omitempty"`
	Proxy    string                     `json:"proxy,omitempty"`
	Extra    map[string]json.RawMessage `json:"-"`
}

Record mirrors the DnsRecord schema. Unknown fields are kept in Extra and written back unchanged (custom JSON marshalling).

func (Record) MarshalJSON

func (r Record) MarshalJSON() ([]byte, error)

MarshalJSON writes known fields, then unknown fields kept in Extra.

func (*Record) UnmarshalJSON

func (r *Record) UnmarshalJSON(b []byte) error

UnmarshalJSON reads known fields and keeps unknown ones in Extra.

type RecordError

type RecordError struct {
	Messages []string `json:"messages"`
	Record   *Record  `json:"record,omitempty"`
	Severity Severity `json:"severity"`
}

RecordError is one entry of ValidationReport.RecordErrors.

type Severity

type Severity string

Severity of a validation problem.

const (
	SeverityError   Severity = "ERROR"
	SeverityWarning Severity = "WARNING"
)

type Snapshot

type Snapshot struct {
	ID        int64  `json:"id"`
	CreatedAt string `json:"createdAt"`
	ZoneName  string `json:"zoneName"`
}

Snapshot mirrors DnsZoneSnapshotItem.

type StaticToken

type StaticToken string

StaticToken is a TokenSource returning a fixed token.

func (StaticToken) Token

func (t StaticToken) Token(context.Context) (string, error)

type TokenSource

type TokenSource interface {
	Token(ctx context.Context) (string, error)
}

TokenSource returns a bearer token (OIDC JWT) for eurodns-gate.

type Transport

type Transport struct {
	Base http.RoundTripper
	// contains filtered or unexported fields
}

Transport is an http.RoundTripper that applies authentication, the shared rate limit (waits, never drops) and retries with backoff on 429/5xx and network errors for idempotent methods. A reverse proxy can use it directly.

func NewTransport

func NewTransport(opts ...Option) *Transport

NewTransport builds a Transport from options (WithAuth, WithRateLimit, WithMaxRetries, WithRetryBackoff, WithUserAgent, WithHTTPClient's transport as base).

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

type ValidationReport

type ValidationReport struct {
	IsValid           *bool             `json:"isValid,omitempty"`
	RecordErrors      []json.RawMessage `json:"recordErrors,omitempty"`
	URLForwardErrors  []json.RawMessage `json:"urlForwardErrors,omitempty"`
	MailForwardErrors []json.RawMessage `json:"mailForwardErrors,omitempty"`
	ZoneErrors        []json.RawMessage `json:"zoneErrors,omitempty"`
}

ValidationReport mirrors ZoneValidationReport; error details are kept raw.

func (*ValidationReport) Errors

func (r *ValidationReport) Errors() []Problem

Errors returns only ERROR-severity problems (warnings do not block a save).

func (*ValidationReport) Problems

func (r *ValidationReport) Problems() []Problem

Problems flattens record, zone and forward errors. A zone error naming several records yields one problem per record. Unparseable entries are reported with their raw JSON as message.

func (*ValidationReport) Valid

func (r *ValidationReport) Valid() bool

Valid reports whether the zone can be saved: isValid is not false and no ERROR-severity problem is present.

type WriteOption

type WriteOption func(*writeOptions)

WriteOption configures a write call.

func WithIfMatch

func WithIfMatch(etag string) WriteOption

WithIfMatch sends If-Match. EuroDNS ignores it; eurodns-gate returns 412 when the zone changed since the given ETag.

type Zone

type Zone struct {
	Name          string                     `json:"name,omitempty"`
	DomainConnect *bool                      `json:"domainConnect,omitempty"`
	Records       []Record                   `json:"records"`
	URLForwards   []json.RawMessage          `json:"urlForwards"`
	MailForwards  []json.RawMessage          `json:"mailForwards"`
	Report        *ValidationReport          `json:"report,omitempty"`
	Extra         map[string]json.RawMessage `json:"-"`

	// ETag is set by GetZone when the server returns one (eurodns-gate does,
	// EuroDNS does not). It is never serialized.
	ETag string `json:"-"`
}

Zone mirrors the DnsZonePage schema. URL and mail forwards are kept as raw JSON so that a zone read then written back is preserved byte for byte.

func (Zone) MarshalJSON

func (z Zone) MarshalJSON() ([]byte, error)

MarshalJSON writes known fields, then unknown fields kept in Extra. Nil records and forwards are written as empty arrays.

func (*Zone) UnmarshalJSON

func (z *Zone) UnmarshalJSON(b []byte) error

UnmarshalJSON reads known fields and keeps unknown ones in Extra.

type ZoneError

type ZoneError struct {
	Message      string            `json:"message"`
	Records      []Record          `json:"records,omitempty"`
	URLForwards  []json.RawMessage `json:"urlForwards,omitempty"`
	MailForwards []json.RawMessage `json:"mailForwards,omitempty"`
	Severity     Severity          `json:"severity"`
}

ZoneError is one entry of ValidationReport.ZoneErrors (conflict between records, e.g. a missing ending dot).

Directories

Path Synopsis
Package eurodnstest provides an in-memory fake of the EuroDNS DNS zone API for tests (records, ids, forwards, check, snapshots, rate limiting).
Package eurodnstest provides an in-memory fake of the EuroDNS DNS zone API for tests (records, ids, forwards, check, snapshots, rate limiting).
Package rrset groups EuroDNS records into RRsets (host + type), the unit of ownership in eurodns-gate and of management in the Terraform provider.
Package rrset groups EuroDNS records into RRsets (host + type), the unit of ownership in eurodns-gate and of management in the Terraform provider.
Package zonediff computes the RRset-level delta between two zone states.
Package zonediff computes the RRset-level delta between two zone states.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL