Compare commits

..

15 Commits

Author SHA1 Message Date
amery 76b40e63c7 Merge pull request 'dns: introduce jpictl dns sync to update public DNS records' (#25)
A and AAAA only

Reviewed-on: #25
2023-09-13 15:31:57 +02:00
amery 5d82de5535 jpictl: introduce jpictl dns sync to update public DNS records
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 20:30:02 +00:00
amery c33d0dab16 jpictl: refactor dns.Manager factory to support Provider data
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 20:30:02 +00:00
amery dd585b0fa2 dns: add Sync() mechanism to update A/AAAA records on the DNS provider
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 20:30:02 +00:00
amery 172752ab90 dns: add RecordSetter and RecordAppender to Provider
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 20:30:02 +00:00
amery 4e2693b12c Merge pull request 'dns: introduce DNS Manager and BIND config writer' (#24)
Reviewed-on: #24
2023-09-12 18:22:52 +02:00
amery eba0340e32 jpictl: introduce jpictl dns write command
it renders BIND config to describe the public view of the cluster

Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 16:21:23 +00:00
amery 1a47985bd7 dns: Manager.WriteTo() generates BIND config, fully qualifies
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:56:07 +00:00
amery f5ea72740c dns: introduce initial DNS Manager
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:56:07 +00:00
amery 357c85dc1a dns: SortRegions() by ISO3166
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:55:38 +00:00
amery 00aec477a4 dns: DefaultDNSProvider() using CLOUDFLARE_DNS_API_TOKEN
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:55:38 +00:00
amery e0d8592dc1 dns: introduce AddrRecord{} to abstract A/AAAA entries
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:55:38 +00:00
amery c397ca29ac cluster: introduce Region interators
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:55:38 +00:00
amery 066788b9be vscode: add Lookuper, publicsuffix and libdns to the dictionary
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:55:38 +00:00
amery 4402555f04 Merge pull request 'cluster: ensure ceph monitors are set when loading a config file' (#23)
Reviewed-on: #23
2023-09-12 15:21:25 +02:00
14 changed files with 813 additions and 334 deletions
+72 -15
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"os"
"time"
"github.com/spf13/cobra"
@@ -10,7 +11,13 @@ import (
"git.jpi.io/amery/jpictl/pkg/dns"
)
func newDNSManager(m *cluster.Cluster) (*dns.Manager, error) {
const (
// DNSSyncTimeout specifies how long are we willing to wait for a DNS
// synchronization
DNSSyncTimeout = 10 * time.Second
)
func newDNSManager(m *cluster.Cluster, provider dns.Provider) (*dns.Manager, error) {
domain := m.Domain
if m.Name != "" {
domain = m.Name + "." + domain
@@ -21,27 +28,48 @@ func newDNSManager(m *cluster.Cluster) (*dns.Manager, error) {
return nil, err
}
if provider != nil {
// set provider only if specified
err = dns.WithProvider(provider)(mgr)
if err != nil {
return nil, err
}
}
if err := populateDNSManager(mgr, m); err != nil {
return nil, err
}
return mgr, nil
}
func populateDNSManager(mgr *dns.Manager, m *cluster.Cluster) error {
var err error
ctx := context.TODO()
m.ForEachZone(func(z *cluster.Zone) bool {
z.ForEachMachine(func(p *cluster.Machine) bool {
err = mgr.AddHost(context.TODO(), z.Name, p.ID, true, p.PublicAddresses...)
err = mgr.AddHost(ctx, z.Name, p.ID, true, p.PublicAddresses...)
return err != nil
})
return err != nil
})
if err != nil {
return err
}
m.ForEachRegion(func(r *cluster.Region) bool {
r.ForEachZone(func(z *cluster.Zone) bool {
err = mgr.AddRegion(ctx, r.Name, z.Name)
return err != nil
})
return err != nil
})
if err != nil {
return nil, err
}
for _, r := range m.Regions {
err := mgr.AddRegion(context.TODO(), r.Name, r.Zones()...)
if err != nil {
return nil, err
}
}
return mgr, nil
return err
}
// Command
@@ -51,6 +79,7 @@ var dnsCmd = &cobra.Command{
var dnsWriteCmd = &cobra.Command{
Use: "write",
Short: "dns write generates public DNS records",
PreRun: setVerbosity,
RunE: func(_ *cobra.Command, _ []string) error {
m, err := cfg.LoadZones(true)
@@ -58,7 +87,7 @@ var dnsWriteCmd = &cobra.Command{
return err
}
mgr, err := newDNSManager(m)
mgr, err := newDNSManager(m, nil)
if err != nil {
return err
}
@@ -68,8 +97,36 @@ var dnsWriteCmd = &cobra.Command{
},
}
var dnsSyncCmd = &cobra.Command{
Use: "sync",
Short: "dns sync updates public DNS records",
PreRun: setVerbosity,
RunE: func(_ *cobra.Command, _ []string) error {
cred, err := dns.DefaultDNSProvider()
if err != nil {
return err
}
m, err := cfg.LoadZones(true)
if err != nil {
return err
}
mgr, err := newDNSManager(m, cred)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), DNSSyncTimeout)
defer cancel()
return mgr.Sync(ctx)
},
}
func init() {
rootCmd.AddCommand(dnsCmd)
dnsCmd.AddCommand(dnsWriteCmd)
dnsCmd.AddCommand(dnsSyncCmd)
}
+2 -2
View File
@@ -2,8 +2,6 @@ module git.jpi.io/amery/jpictl
go 1.19
replace asciigoat.org/ini => ../../../asciigoat.org/ini
require (
asciigoat.org/ini v0.2.5
darvaza.org/core v0.9.8
@@ -19,6 +17,7 @@ require (
github.com/spf13/cobra v1.7.0
golang.org/x/crypto v0.12.0
golang.org/x/net v0.14.0
gopkg.in/gcfg.v1 v1.2.3
gopkg.in/yaml.v3 v3.0.1
)
@@ -46,4 +45,5 @@ require (
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.12.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
+6
View File
@@ -1,5 +1,7 @@
asciigoat.org/core v0.3.9 h1:hgDDz4ecm3ZvehX++m8A/IzAt+B5oDPiRtxatzfUHPQ=
asciigoat.org/core v0.3.9/go.mod h1:CAaHwyw8MpAq4a1MYtN2dxJrsK+hmIdW50OndaQZYPI=
asciigoat.org/ini v0.2.5 h1:4gRIp9rU+XQt8+HMqZO5R7GavMv9Yl2+N+je6djDIAE=
asciigoat.org/ini v0.2.5/go.mod h1:gmXzJ9XFqf1NLk5nQkj04USQ4tMtdRJHNQX6vp3DzjU=
darvaza.org/core v0.9.8 h1:luLxgfUc2pzuusYPo/Z/dC/qr9XZPKpSQw8/kS7zNUM=
darvaza.org/core v0.9.8/go.mod h1:Dbme64naxeshQfxcVJX9ZT7AiGyIY8kldfuELVtf8mw=
darvaza.org/resolver v0.5.4 h1:dlSBNV14yYsp7Kg7ipwYOMNsLbrpeXa8Z0HBTa0Ryxs=
@@ -101,6 +103,10 @@ golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+39 -6
View File
@@ -1,5 +1,10 @@
package cluster
var (
_ MachineIterator = (*Region)(nil)
_ ZoneIterator = (*Region)(nil)
)
// Region represents a group of zones geographically related
type Region struct {
m *Cluster
@@ -9,13 +14,41 @@ type Region struct {
Regions []string `json:",omitempty" yaml:",omitempty"`
}
// Zones ...
func (r *Region) Zones() []string {
out := make([]string, len(r.zones))
for i := range r.zones {
out[i] = r.zones[i].Name
// ForEachRegion calls a function for each Region of the cluster
// until instructed to terminate the loop
func (m *Cluster) ForEachRegion(fn func(r *Region) bool) {
for i := range m.Regions {
r := &m.Regions[i]
if fn(r) {
return
}
}
}
// ForEachMachine calls a function for each Machine in the region
// until instructed to terminate the loop
func (r *Region) ForEachMachine(fn func(*Machine) bool) {
r.ForEachZone(func(z *Zone) bool {
var term bool
z.ForEachMachine(func(p *Machine) bool {
term = fn(p)
return term
})
return term
})
}
// ForEachZone calls a function for each Zone in the region
// until instructed to terminate the loop
func (r *Region) ForEachZone(fn func(*Zone) bool) {
for _, p := range r.zones {
if fn(p) {
// terminate
return
}
}
return out
}
func (m *Cluster) initRegions(_ *ScanOptions) error {
+25 -21
View File
@@ -1,34 +1,38 @@
// Package dns manages DNS entries for the cluster
package dns
import "net/netip"
import (
"fmt"
"net/netip"
)
// // A Config defines a Region
//
// type Config struct {
// // Name is the identifier of this Region
// Name string
// // Regions are a list of (sub)regions that belong to this Region
// Regions []string
// // Zones are a list of Zones that directly belong to this Region
// Zones []string
// }
//
// type Region struct {
// Name string
// }
// Zone represents a set of machines with high affinity
// Zone represents a set of hosts with high affinity
type Zone struct {
Name string
Machines map[int]*Machine
Hosts map[int]*Host
}
// Machine represents a member of the cluster
type Machine struct {
ID int
func (z *Zone) String() string {
if z == nil {
return "undetermined"
}
return z.Name
}
// Host represents a member of the cluster
type Host struct {
zone *Zone
ID int
Active bool
Addrs []netip.Addr
}
func (p *Host) String() string {
if p == nil {
return "undetermined"
}
return fmt.Sprintf("%s-%v", p.zone, p.ID)
}
-41
View File
@@ -1,41 +0,0 @@
package dns
import (
"context"
"io/fs"
"net/netip"
)
// AddHost registers a machine
func (mgr *Manager) AddHost(_ context.Context, zone string, id int,
active bool, addrs ...netip.Addr) error {
//
if zone == "" || id <= 0 {
return fs.ErrInvalid
}
z, ok := mgr.zones[zone]
if !ok {
z = &Zone{
Name: zone,
Machines: make(map[int]*Machine),
}
mgr.zones[zone] = z
}
z.Machines[id] = &Machine{
ID: id,
Active: active,
Addrs: SortAddrSlice(addrs),
}
return nil
}
// AddRegion specifies a new region and the zones it contains
func (mgr *Manager) AddRegion(_ context.Context, region string, zones ...string) error {
mgr.l.Debug().WithField("region", region).WithField("zones", zones).Print()
mgr.regions[region] = append(mgr.regions[region], zones...)
return nil
}
+60 -11
View File
@@ -1,9 +1,11 @@
package dns
import (
"context"
"errors"
"io/fs"
"net/netip"
"strings"
"sync"
"darvaza.org/core"
"darvaza.org/slog"
@@ -13,8 +15,6 @@ import (
// Manager is a DNS Manager instance
type Manager struct {
mu sync.Mutex
domain string
suffix string
zones map[string]*Zone
@@ -29,10 +29,7 @@ type ManagerOption func(*Manager) error
func newErrorManagerOption(err error, hint string) ManagerOption {
return func(*Manager) error {
if hint != "" {
err = core.Wrap(err, hint)
}
return err
return core.Wrap(err, hint)
}
}
@@ -77,9 +74,6 @@ func (mgr *Manager) setDefaults() error {
return errors.New("domain not specified")
}
mgr.zones = make(map[string]*Zone)
mgr.regions = make(map[string][]string)
for _, opt := range opts {
if err := opt(mgr); err != nil {
return err
@@ -109,7 +103,10 @@ func WithDomain(domain string) ManagerOption {
// NewManager creates a DNS manager with the
func NewManager(opts ...ManagerOption) (*Manager, error) {
mgr := new(Manager)
mgr := &Manager{
zones: make(map[string]*Zone),
regions: make(map[string][]string),
}
for _, opt := range opts {
if err := opt(mgr); err != nil {
@@ -122,3 +119,55 @@ func NewManager(opts ...ManagerOption) (*Manager, error) {
}
return mgr, nil
}
// AddHost registers a host
func (mgr *Manager) AddHost(_ context.Context, zone string, id int,
active bool, addrs ...netip.Addr) error {
//
if zone == "" || id <= 0 {
return fs.ErrInvalid
}
z, ok := mgr.zones[zone]
if !ok {
z = &Zone{
Name: zone,
Hosts: make(map[int]*Host),
}
mgr.zones[zone] = z
}
p := &Host{
zone: z,
ID: id,
Active: active,
Addrs: SortAddrSlice(addrs),
}
z.Hosts[id] = p
if log, ok := mgr.l.Debug().WithEnabled(); ok {
log.WithField("subsystem", "dns").
WithField("zone", zone).
WithField("host", p.String()).
WithField("active", active).
Print()
}
return nil
}
// AddRegion specifies a new region and the zones it contains
func (mgr *Manager) AddRegion(_ context.Context, region string, zones ...string) error {
mgr.regions[region] = append(mgr.regions[region], zones...)
if log, ok := mgr.l.Debug().WithEnabled(); ok {
for _, zoneName := range zones {
log.WithField("subsystem", "dns").
WithField("region", region).
WithField("zone", zoneName).Print()
}
}
return nil
}
+2
View File
@@ -18,6 +18,8 @@ const (
type Provider interface {
libdns.RecordGetter
libdns.RecordDeleter
libdns.RecordSetter
libdns.RecordAppender
}
// DefaultDNSProvider returns a cloudflare DNS provider
+108 -16
View File
@@ -1,7 +1,9 @@
package dns
import (
"bytes"
"fmt"
"io"
"net/netip"
"sort"
"time"
@@ -10,7 +12,11 @@ import (
"github.com/libdns/libdns"
)
// SortAddrSlice ...
func (mgr *Manager) fqdn(name string) string {
return fmt.Sprintf("%s.%s.", name, mgr.domain)
}
// SortAddrSlice sorts a slice of [netip.Addr]
func SortAddrSlice(s []netip.Addr) []netip.Addr {
sort.Slice(s, func(i, j int) bool {
return s[i].Less(s[j])
@@ -18,6 +24,46 @@ func SortAddrSlice(s []netip.Addr) []netip.Addr {
return s
}
// SortAddrRecords sorts a slice of [AddrRecord]
// by Name and Address
func SortAddrRecords(s []AddrRecord) []AddrRecord {
sort.Slice(s, func(i, j int) bool {
return s[i].Name < s[j].Name
})
for _, p := range s {
SortAddrSlice(p.Addr)
}
return s
}
// SortRegions sorts regions. first by length those 3-character
// or shorter, and then by length. It's mostly aimed at
// supporting ISO-3166 order
func SortRegions(regions []string) []string {
sort.Slice(regions, func(i, j int) bool {
r1, r2 := regions[i], regions[j]
switch {
case len(r1) < 4:
switch {
case len(r1) < len(r2):
return true
case len(r1) > len(r2):
return false
default:
return r1 < r2
}
case len(r2) < 4:
return false
default:
return r1 < r2
}
})
return regions
}
// AddrRecord represents an A or AAAA record
type AddrRecord struct {
Name string
@@ -44,6 +90,43 @@ func (rr *AddrRecord) Export() []libdns.Record {
return out
}
// WriteTo writes the record in BIND notation
func (rr *AddrRecord) WriteTo(w io.Writer) (int64, error) {
var total int
for _, addr := range rr.Addr {
n, err := fmt.Fprint(w,
rr.Name, "\t",
1, "\t",
core.IIf(addr.Is6(), "AAAA", "A"), "\t",
addr.String(), "\n")
switch {
case err != nil:
return 0, err
case n > 0:
total += n
}
}
return int64(total), nil
}
// String converts the record into BIND entries
func (rr *AddrRecord) String() string {
var buf bytes.Buffer
_, _ = rr.WriteTo(&buf)
return buf.String()
}
func (mgr *Manager) genRegionsSorted() []string {
regions := make([]string, 0, len(mgr.regions))
for name := range mgr.regions {
regions = append(regions, name)
}
return SortRegions(regions)
}
func (mgr *Manager) genAllAddrRecords() []AddrRecord {
var out []AddrRecord
@@ -51,8 +134,8 @@ func (mgr *Manager) genAllAddrRecords() []AddrRecord {
// zones
for _, z := range mgr.zones {
// machines
s := mgr.genZoneMachineRecords(z)
// hosts
s := mgr.genZoneHostRecords(z)
out = append(out, s...)
// zone alias
@@ -68,11 +151,10 @@ func (mgr *Manager) genAllAddrRecords() []AddrRecord {
cache[name] = addrs
}
// regions
for name, zones := range mgr.regions {
for _, name := range mgr.genRegionsSorted() {
var addrs []netip.Addr
for _, z := range zones {
for _, z := range mgr.regions[name] {
addrs = append(addrs, cache[z]...)
}
@@ -85,18 +167,14 @@ func (mgr *Manager) genAllAddrRecords() []AddrRecord {
out = append(out, rec)
}
// sort
sort.Slice(out, func(i, j int) bool {
return out[i].Name < out[j].Name
})
SortAddrRecords(out)
return out
}
func (*Manager) genZoneAddresses(z *Zone) []netip.Addr {
var out []netip.Addr
for _, p := range z.Machines {
for _, p := range z.Hosts {
if p.Active {
out = append(out, p.Addrs...)
}
@@ -106,16 +184,30 @@ func (*Manager) genZoneAddresses(z *Zone) []netip.Addr {
return out
}
func (mgr *Manager) genZoneMachineRecords(z *Zone) []AddrRecord {
out := make([]AddrRecord, 0, len(z.Machines))
func (mgr *Manager) genZoneHostRecords(z *Zone) []AddrRecord {
out := make([]AddrRecord, 0, len(z.Hosts))
for _, p := range z.Machines {
for _, p := range z.Hosts {
rec := AddrRecord{
Name: fmt.Sprintf("%s-%v%s", z.Name, p.ID, mgr.suffix),
Name: p.String() + mgr.suffix,
Addr: p.Addrs,
}
out = append(out, rec)
}
SortAddrRecords(out)
return out
}
func (mgr *Manager) genRegionAddressesCached(name string,
zones map[string][]netip.Addr) []netip.Addr {
//
var addrs []netip.Addr
for _, zoneName := range mgr.regions[name] {
addrs = append(addrs, zones[zoneName]...)
}
SortAddrSlice(addrs)
return addrs
}
+347
View File
@@ -0,0 +1,347 @@
package dns
import (
"context"
"errors"
"net/netip"
"sort"
"strings"
"time"
"darvaza.org/core"
"darvaza.org/slog"
"github.com/libdns/libdns"
)
// SyncAddrRecord is similar to AddrRecord but include libdns.Record details
// fetched from the Provider
type SyncAddrRecord struct {
Name string
Addrs []SyncAddr
}
// SyncAddr extends netip.Addr with ID and TTL fetched from the Provider
type SyncAddr struct {
ID string
Addr netip.Addr
TTL time.Duration
}
// Export assembles a libdns.Record
func (rec *SyncAddr) Export(name string) libdns.Record {
return libdns.Record{
ID: rec.ID,
Name: name,
Type: core.IIf(rec.Addr.Is6(), "AAAA", "A"),
TTL: time.Second,
Value: rec.Addr.String(),
}
}
// SortSyncAddrSlice sorts a slice of [SyncAddr] by its address
func SortSyncAddrSlice(s []SyncAddr) []SyncAddr {
sort.Slice(s, func(i, j int) bool {
a1 := s[i].Addr
a2 := s[j].Addr
return a1.Less(a2)
})
return s
}
// GetRecords pulls all the address records on DNS for our domain
func (mgr *Manager) GetRecords(ctx context.Context) ([]SyncAddrRecord, error) {
if mgr.p == nil {
return nil, errors.New("dns provider not specified")
}
recs, err := mgr.p.GetRecords(ctx, mgr.domain)
if err != nil {
return nil, err
}
return mgr.filteredRecords(recs)
}
// AsSyncAddr converts a A or AAAA [libdns.Record] into a [SyncAddr]
func (mgr *Manager) AsSyncAddr(rr libdns.Record) (SyncAddr, bool, error) {
var out SyncAddr
var addr netip.Addr
// skip non-address types
if rr.Type != "A" && rr.Type != "AAAA" {
return out, false, nil
}
// skip entries not containing our suffix
if mgr.suffix != "" {
if !strings.HasSuffix(rr.Name, mgr.suffix) {
return out, false, nil
}
}
err := addr.UnmarshalText([]byte(rr.Value))
if err != nil {
// invalid address on A or AAAA record
return out, false, err
}
out = SyncAddr{
ID: rr.ID,
TTL: rr.TTL,
Addr: addr,
}
return out, true, nil
}
func (mgr *Manager) filteredRecords(recs []libdns.Record) ([]SyncAddrRecord, error) {
// filter and convert
cache := make(map[string][]SyncAddr)
for _, rr := range recs {
addr, ok, err := mgr.AsSyncAddr(rr)
switch {
case err != nil:
// skip invalid addresses
mgr.l.Error().
WithField("subsystem", "dns").
WithField(slog.ErrorFieldName, err).
WithField("name", rr.Name).
WithField("type", rr.Type).
WithField("addr", rr.Value).
Print()
case ok:
// store
cache[rr.Name] = append(cache[rr.Name], addr)
}
}
// prepare records
out := make([]SyncAddrRecord, len(cache))
names := make([]string, 0, len(cache))
for name := range cache {
names = append(names, name)
}
sort.Strings(names)
for i, name := range names {
addrs := cache[name]
out[i] = SyncAddrRecord{
Name: name,
Addrs: SortSyncAddrSlice(addrs),
}
}
return out, nil
}
// Sync updates all the address records on DNS for our domain
func (mgr *Manager) Sync(ctx context.Context) error {
current, err := mgr.GetRecords(ctx)
if err != nil {
return core.Wrap(err, "GetRecords")
}
goal := mgr.genAllAddrRecords()
for _, p := range makeSyncMap(current, goal) {
err := mgr.doSync(ctx, p.Name, p.Before, p.After)
if err != nil {
return err
}
}
return nil
}
func (mgr *Manager) doSync(ctx context.Context, name string,
before []SyncAddr, after []netip.Addr) error {
//
var err error
for _, a := range after {
before, err = mgr.doSyncUpdateOrInsert(ctx, name, a, before)
if err != nil {
return err
}
}
for _, b := range before {
err = mgr.doSyncRemove(ctx, name, b)
if err != nil {
return err
}
}
return nil
}
func (mgr *Manager) doSyncUpdateOrInsert(ctx context.Context, name string,
addr netip.Addr, addrs []SyncAddr) ([]SyncAddr, error) {
//
var err error
i, ok := findSyncAddrSorted(addr, addrs)
if ok {
rec := addrs[i]
addrs = append(addrs[:i], addrs[i+1:]...)
err = mgr.doSyncUpdate(ctx, name, addr, rec)
} else {
err = mgr.doSyncInsert(ctx, name, addr)
}
return addrs, err
}
func (mgr *Manager) doSyncUpdate(ctx context.Context, name string,
addr netip.Addr, rec SyncAddr) error {
//
var log slog.Logger
var msg string
var err error
if rec.TTL != time.Second {
// amend TTL
// TODO: batch updates
_, err = mgr.p.SetRecords(ctx, mgr.domain, []libdns.Record{
rec.Export(name),
})
if err == nil {
log = mgr.l.Info()
msg = "Updated"
} else {
log = mgr.l.Error().
WithField(slog.ErrorFieldName, err)
msg = "Failed"
}
} else {
log = mgr.l.Info()
msg = "OK"
}
log.
WithField("subsystem", "dns").
WithField("name", name).
WithField("addr", addr).
Print(msg)
return err
}
func (mgr *Manager) doSyncInsert(ctx context.Context, name string,
addr netip.Addr) error {
//
var log slog.Logger
var msg string
rec := libdns.Record{
Name: name,
Type: core.IIf(addr.Is6(), "AAAA", "A"),
TTL: time.Second,
Value: addr.String(),
}
_, err := mgr.p.AppendRecords(ctx, mgr.domain, []libdns.Record{
rec,
})
if err != nil {
log = mgr.l.Error().
WithField(slog.ErrorFieldName, err)
msg = "Failed to Add"
} else {
log = mgr.l.Info()
msg = "Added"
}
log.
WithField("subsystem", "dns").
WithField("name", name).
WithField("addr", addr).
Print(msg)
return err
}
func (mgr *Manager) doSyncRemove(ctx context.Context, name string,
rec SyncAddr) error {
//
var log slog.Logger
var msg string
// TODO: batch deletes
_, err := mgr.p.DeleteRecords(ctx, mgr.domain, []libdns.Record{
rec.Export(name),
})
if err != nil {
log = mgr.l.Error().
WithField(slog.ErrorFieldName, err)
msg = "Failed to Delete"
} else {
log = mgr.l.Warn()
msg = "Deleted"
}
log.
WithField("subsystem", "dns").
WithField("name", name).
WithField("addr", rec.Addr).
Print(msg)
return err
}
func findSyncAddrSorted(target netip.Addr, addrs []SyncAddr) (int, bool) {
for i, a := range addrs {
switch target.Compare(a.Addr) {
case 0:
// match
return i, true
case -1:
// miss
return -1, false
default:
// next
}
}
return -1, false
}
type syncMapEntry struct {
Name string
Before []SyncAddr
After []netip.Addr
}
func makeSyncMap(current []SyncAddrRecord,
goal []AddrRecord) map[string]syncMapEntry {
//
data := make(map[string]syncMapEntry)
for _, cur := range current {
me, ok := data[cur.Name]
if !ok {
me = syncMapEntry{
Name: cur.Name,
}
}
me.Before = append(me.Before, cur.Addrs...)
data[cur.Name] = me
}
for _, rr := range goal {
me, ok := data[rr.Name]
if !ok {
me = syncMapEntry{
Name: rr.Name,
}
}
me.After = append(me.After, rr.Addr...)
data[rr.Name] = me
}
return data
}
+42 -15
View File
@@ -4,32 +4,59 @@ import (
"bytes"
"fmt"
"io"
"time"
"github.com/libdns/libdns"
"net/netip"
)
// WriteTo writes the DNS data for the cluster
func (mgr *Manager) WriteTo(w io.Writer) (int64, error) {
var buf bytes.Buffer
records := mgr.genAllAddrRecords()
for i := range records {
r := &records[i]
r.Name = fmt.Sprintf("%s.%s.", r.Name, mgr.domain)
cache := make(map[string][]netip.Addr)
// zones
for _, z := range mgr.zones {
mgr.writeZoneHosts(&buf, z)
// zone alias
addrs := mgr.genZoneAddresses(z)
zoneName := z.Name
rr := AddrRecord{
Name: mgr.fqdn(zoneName + mgr.suffix),
Addr: addrs,
}
_, _ = rr.WriteTo(&buf)
// and cache for regions
cache[zoneName] = addrs
}
for _, rr1 := range records {
for _, rr2 := range rr1.Export() {
writeRecord(&buf, rr2)
}
// regions, sorted
for _, name := range mgr.genRegionsSorted() {
addrs := mgr.genRegionAddressesCached(name, cache)
mgr.writeRegionAddresses(&buf, name, addrs)
}
return buf.WriteTo(w)
}
func writeRecord(w io.Writer, rr libdns.Record) {
_, _ = fmt.Fprintf(w, "%s\t%v\tIN\t%s\t%s\n",
rr.Name, int(rr.TTL/time.Second),
rr.Type, rr.Value)
func (mgr *Manager) writeZoneHosts(w io.Writer, z *Zone) {
_, _ = fmt.Fprintf(w, ";\n; %s\n;\n", z.Name)
for _, rr := range mgr.genZoneHostRecords(z) {
rr.Name = mgr.fqdn(rr.Name)
_, _ = rr.WriteTo(w)
}
}
func (mgr *Manager) writeRegionAddresses(w io.Writer, name string, addrs []netip.Addr) {
_, _ = fmt.Fprintf(w, "; %s\n", name)
rr := AddrRecord{
Name: mgr.fqdn(name + mgr.suffix),
Addr: addrs,
}
_, _ = rr.WriteTo(w)
}
+110 -14
View File
@@ -2,6 +2,7 @@ package wireguard
import (
"bytes"
"errors"
"fmt"
"io"
"net/netip"
@@ -9,8 +10,8 @@ import (
"strings"
"text/template"
"asciigoat.org/ini/basic"
"darvaza.org/core"
"gopkg.in/gcfg.v1"
)
var configTemplate = template.Must(template.New("config").Funcs(template.FuncMap{
@@ -106,11 +107,6 @@ func (ep EndpointAddress) String() string {
}
}
// UnmarshalText loads an endpoint address from text data
func (ep *EndpointAddress) UnmarshalText(b []byte) error {
return ep.FromString(string(b))
}
// FromString sets the EndpointAddress from a given "[host]:port"
func (ep *EndpointAddress) FromString(s string) error {
host, port, err := core.SplitHostPort(s)
@@ -131,6 +127,98 @@ func (ep *EndpointAddress) FromString(s string) error {
return nil
}
type intermediateConfig struct {
Interface interfaceConfig
Peer peersConfig
}
func (v *intermediateConfig) Export() (*Config, error) {
var out Config
var err error
// Interface
out.Interface, err = v.Interface.Export()
if err != nil {
return nil, err
}
// Peers
peers, ok := v.PeersCount()
if !ok {
return nil, errors.New("inconsistent Peer data")
}
for i := 0; i < peers; i++ {
p, err := v.ExportPeer(i)
if err != nil {
err = core.Wrapf(err, "Peer[%v]:", i)
return nil, err
}
out.Peer = append(out.Peer, p)
}
return &out, nil
}
type interfaceConfig struct {
Address netip.Addr
PrivateKey string
ListenPort uint16
}
func (p interfaceConfig) Export() (InterfaceConfig, error) {
var err error
out := InterfaceConfig{
Address: p.Address,
ListenPort: p.ListenPort,
}
out.PrivateKey, err = PrivateKeyFromBase64(p.PrivateKey)
if err != nil {
err = core.Wrap(err, "PrivateKey")
return InterfaceConfig{}, err
}
return out, nil
}
type peersConfig struct {
PublicKey []string
Endpoint []string
AllowedIPs []string
}
func (v *intermediateConfig) ExportPeer(i int) (PeerConfig, error) {
var out PeerConfig
// Endpoint
s := v.Peer.Endpoint[i]
err := out.Endpoint.FromString(s)
if err != nil {
err = core.Wrap(err, "Endpoint")
return out, err
}
// PublicKey
out.PublicKey, err = PublicKeyFromBase64(v.Peer.PublicKey[i])
if err != nil {
err = core.Wrap(err, "PublicKey")
return out, err
}
// AllowedIPs
s = v.Peer.AllowedIPs[i]
out.AllowedIPs, err = parseAllowedIPs(s)
if err != nil {
err = core.Wrap(err, "AllowedIPs")
return out, err
}
return out, nil
}
func parseAllowedIPs(data string) ([]netip.Prefix, error) {
var out []netip.Prefix
@@ -147,17 +235,25 @@ func parseAllowedIPs(data string) ([]netip.Prefix, error) {
return out, nil
}
func (v *intermediateConfig) PeersCount() (int, bool) {
c0 := len(v.Peer.Endpoint)
c1 := len(v.Peer.PublicKey)
c2 := len(v.Peer.AllowedIPs)
if c0 != c1 || c1 != c2 {
return 0, false
}
return c0, true
}
// NewConfigFromReader parses a wgN.conf file
func NewConfigFromReader(r io.Reader) (*Config, error) {
doc, err := basic.Decode(r)
if err != nil {
temp := &intermediateConfig{}
if err := gcfg.ReadInto(temp, r); err != nil {
return nil, err
}
cfg, err := newConfigFromDocument(doc)
if err != nil {
return nil, err
}
return cfg, nil
return temp.Export()
}
-169
View File
@@ -1,169 +0,0 @@
package wireguard
import (
"io/fs"
"strconv"
"asciigoat.org/ini/basic"
"darvaza.org/core"
)
type sectionHandler func(*Config, *basic.Section) error
var sectionMap = map[string]func(*Config, *basic.Section) error{
"Interface": loadInterfaceConfSection,
"Peer": loadPeerConfSection,
}
func loadConfSection(out *Config, src *basic.Section) error {
h, ok := sectionMap[src.Key]
if !ok {
return core.Wrapf(fs.ErrInvalid, "unknown section %q", src.Key)
}
return h(out, src)
}
func loadInterfaceConfSection(out *Config, src *basic.Section) error {
var cfg InterfaceConfig
for _, field := range src.Fields {
if err := loadInterfaceConfField(&cfg, field); err != nil {
return core.Wrap(err, "Interface")
}
}
out.Interface = cfg
return nil
}
func loadPeerConfSection(out *Config, src *basic.Section) error {
var cfg PeerConfig
for _, field := range src.Fields {
if err := loadPeerConfField(&cfg, field); err != nil {
return core.Wrapf(err, "Peer[%v]", len(out.Peer))
}
}
out.Peer = append(out.Peer, cfg)
return nil
}
// revive:disable:cyclomatic
// revive:disable:cognitive-complexity
func loadInterfaceConfField(cfg *InterfaceConfig, field basic.Field) error {
// revive:enable:cyclomatic
// revive:enable:cognitive-complexity
// TODO: refactor when asciigoat's ini parser learns to do reflection
switch field.Key {
case "Address":
if !core.IsZero(cfg.Address) {
return core.Wrapf(fs.ErrInvalid, "duplicate field %q", field.Key)
}
err := cfg.Address.UnmarshalText([]byte(field.Value))
switch {
case err != nil:
return core.Wrap(err, field.Key)
default:
return nil
}
case "PrivateKey":
if !core.IsZero(cfg.PrivateKey) {
return core.Wrapf(fs.ErrInvalid, "duplicate field %q", field.Key)
}
err := cfg.PrivateKey.UnmarshalText([]byte(field.Value))
switch {
case err != nil:
return core.Wrap(err, field.Key)
default:
return nil
}
case "ListenPort":
if cfg.ListenPort > 0 {
return core.Wrapf(fs.ErrInvalid, "duplicate field %q", field.Key)
}
u64, err := strconv.ParseUint(field.Value, 10, 16)
switch {
case err != nil:
return core.Wrap(err, field.Key)
case u64 == 0:
return core.Wrapf(fs.ErrInvalid, "invalid %q value", field.Key)
default:
cfg.ListenPort = uint16(u64)
return nil
}
default:
return core.Wrapf(fs.ErrInvalid, "unknown field %q", field.Key)
}
}
// revive:disable:cyclomatic
// revive:disable:cognitive-complexity
func loadPeerConfField(cfg *PeerConfig, field basic.Field) error {
// revive:enable:cyclomatic
// revive:enable:cognitive-complexity
switch field.Key {
case "PublicKey":
if !core.IsZero(cfg.PublicKey) {
return core.Wrapf(fs.ErrInvalid, "duplicate field %q", field.Key)
}
err := cfg.PublicKey.UnmarshalText([]byte(field.Value))
switch {
case err != nil:
return core.Wrap(err, field.Key)
default:
return nil
}
case "Endpoint":
if cfg.Endpoint.String() != "" {
return core.Wrapf(fs.ErrInvalid, "duplicate field %q", field.Key)
}
err := cfg.Endpoint.UnmarshalText([]byte(field.Value))
switch {
case err != nil:
return core.Wrap(err, field.Key)
default:
return nil
}
case "AllowedIPs":
s, err := parseAllowedIPs(field.Value)
switch {
case err != nil:
return core.Wrap(err, field.Key)
case len(s) > 0:
cfg.AllowedIPs = append(cfg.AllowedIPs, s...)
return nil
}
default:
return core.Wrapf(fs.ErrInvalid, "unknown field %q", field.Key)
}
return nil
}
func newConfigFromDocument(doc *basic.Document) (*Config, error) {
var out Config
if len(doc.Global) > 0 {
err := core.Wrap(fs.ErrInvalid, "fields before the first section")
return nil, err
}
for i := range doc.Sections {
src := &doc.Sections[i]
if err := loadConfSection(&out, src); err != nil {
return nil, err
}
}
return &out, nil
}
-24
View File
@@ -51,30 +51,6 @@ func (pub PublicKey) String() string {
}
}
// UnmarshalText loads the value from base64
func (key *PrivateKey) UnmarshalText(b []byte) error {
v, err := PrivateKeyFromBase64(string(b))
switch {
case err != nil:
return err
default:
*key = v
return nil
}
}
// UnmarshalText loads the value from base64
func (pub *PublicKey) UnmarshalText(b []byte) error {
v, err := PublicKeyFromBase64(string(b))
switch {
case err != nil:
return err
default:
*pub = v
return nil
}
}
// MarshalJSON encodes the key for JSON, omitting empty.
func (key PrivateKey) MarshalJSON() ([]byte, error) {
return encodeKeyJSON(key.String())