Files
go-dns/update.go
T

104 lines
2.8 KiB
Go

package godns
import (
"context"
"fmt"
"time"
"github.com/miekg/dns"
)
// Action is one RFC 2136 UPDATE operation.
type Action int
const (
// ActionAdd inserts a record.
ActionAdd Action = iota
// ActionDelete removes one record that matches owner, type, and rdata.
ActionDelete
)
// Change is one add or delete of one record inside an Update batch.
type Change struct {
Action Action
Record Record
}
// Add builds an insert change for rec.
func Add(rec Record) Change {
return Change{Action: ActionAdd, Record: rec}
}
// Delete builds a delete of one matching record (owner, type, and rdata).
func Delete(rec Record) Change {
return Change{Action: ActionDelete, Record: rec}
}
// UpdateResult is a successful UPDATE response.
// Rcode is the nameserver rcode text, which is NOERROR on success.
type UpdateResult struct {
Rcode string
}
// Update sends an RFC 2136 UPDATE for changes, signed with the session TSIG key.
// It tries UDP, then TCP when the UDP exchange fails or the response is truncated.
// No changes returns success and does not contact the nameserver.
// It returns an error when ctx is done, a record cannot be encoded, the
// exchange fails, the nameserver rcode is not NOERROR, or Action is neither
// ActionAdd nor ActionDelete.
func (c *Client) Update(ctx context.Context, changes ...Change) (*UpdateResult, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if len(changes) == 0 {
return &UpdateResult{Rcode: dns.RcodeToString[dns.RcodeSuccess]}, nil
}
msg, err := c.updateMessage(changes)
if err != nil {
return nil, err
}
client := &dns.Client{
Net: "udp",
TsigSecret: map[string]string{dns.Fqdn(c.tsig.Name): c.tsig.Secret},
Timeout: timeoutFrom(ctx),
}
udpMsg := msg.Copy()
resp, _, err := client.ExchangeContext(ctx, udpMsg, c.server)
if err != nil || (resp != nil && resp.Truncated) {
client.Net = "tcp"
resp, _, err = client.ExchangeContext(ctx, msg.Copy(), c.server)
}
if err != nil {
return nil, fmt.Errorf("UPDATE to %s: %w", c.server, err)
}
if resp.Rcode != dns.RcodeSuccess {
return nil, fmt.Errorf("UPDATE rcode %s", dns.RcodeToString[resp.Rcode])
}
return &UpdateResult{Rcode: dns.RcodeToString[resp.Rcode]}, nil
}
// updateMessage builds a TSIG-signed RFC 2136 UPDATE message.
func (c *Client) updateMessage(changes []Change) (*dns.Msg, error) {
zone := dns.Fqdn(c.zone)
msg := new(dns.Msg)
msg.SetUpdate(zone)
for _, change := range changes {
rr, err := recordToRR(change.Record)
if err != nil {
return nil, err
}
switch change.Action {
case ActionAdd:
msg.Insert([]dns.RR{rr})
case ActionDelete:
msg.Remove([]dns.RR{rr})
default:
return nil, fmt.Errorf("unsupported update action %d", change.Action)
}
}
msg.SetTsig(dns.Fqdn(c.tsig.Name), tsigAlgorithm(c.tsig.Algorithm), 300, time.Now().Unix())
return msg, nil
}