Compare commits

..

21 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
amery 6e7f24f491 cluster: ensure ceph monitors are set when loading a config file
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 12:47:24 +00:00
amery 54b302c6d5 vscode: add asciigoat, cyclomatic and Wrapf to the dictionary
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 12:47:18 +00:00
amery f62a47003d Merge pull request 'cluster: introduce Regions to group zones' (#22)
Reviewed-on: #22
2023-09-12 14:45:01 +02:00
amery 5abaed9047 Merge pull request 'jpictl: fix verbosity handling' (#21)
Reviewed-on: #21
2023-09-12 14:43:40 +02:00
amery c702d649e0 cluster: introduce Regions to group zones
only available via config-file

Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 02:17:50 +00:00
amery e9f9d474dc jpictl: fix verbosity handling
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 01:38:12 +00:00
20 changed files with 1121 additions and 564 deletions
+1
View File
@@ -8,6 +8,7 @@
"jpictl", "jpictl",
"libdns", "libdns",
"Lookuper", "Lookuper",
"publicsuffix",
"Wrapf", "Wrapf",
"zerolog" "zerolog"
] ]
+65 -9
View File
@@ -12,10 +12,12 @@ import (
) )
const ( const (
DNSTimeout = 10 * time.Second // DNSSyncTimeout specifies how long are we willing to wait for a DNS
// synchronization
DNSSyncTimeout = 10 * time.Second
) )
func newDNSManager(ctx context.Context, m *cluster.Cluster) (*dns.Manager, error) { func newDNSManager(m *cluster.Cluster, provider dns.Provider) (*dns.Manager, error) {
domain := m.Domain domain := m.Domain
if m.Name != "" { if m.Name != "" {
domain = m.Name + "." + domain domain = m.Name + "." + domain
@@ -26,6 +28,26 @@ func newDNSManager(ctx context.Context, m *cluster.Cluster) (*dns.Manager, error
return nil, err 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 { m.ForEachZone(func(z *cluster.Zone) bool {
z.ForEachMachine(func(p *cluster.Machine) bool { z.ForEachMachine(func(p *cluster.Machine) bool {
err = mgr.AddHost(ctx, z.Name, p.ID, true, p.PublicAddresses...) err = mgr.AddHost(ctx, z.Name, p.ID, true, p.PublicAddresses...)
@@ -34,12 +56,20 @@ func newDNSManager(ctx context.Context, m *cluster.Cluster) (*dns.Manager, error
return err != nil return err != nil
}) })
if err != nil { if err != nil {
return nil, err return err
} }
return mgr, nil 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
})
return err
} }
// Command // Command
@@ -49,6 +79,7 @@ var dnsCmd = &cobra.Command{
var dnsWriteCmd = &cobra.Command{ var dnsWriteCmd = &cobra.Command{
Use: "write", Use: "write",
Short: "dns write generates public DNS records",
PreRun: setVerbosity, PreRun: setVerbosity,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
m, err := cfg.LoadZones(true) m, err := cfg.LoadZones(true)
@@ -56,10 +87,7 @@ var dnsWriteCmd = &cobra.Command{
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), DNSTimeout) mgr, err := newDNSManager(m, nil)
defer cancel()
mgr, err := newDNSManager(ctx, m)
if err != nil { if err != nil {
return err return err
} }
@@ -69,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() { func init() {
rootCmd.AddCommand(dnsCmd) rootCmd.AddCommand(dnsCmd)
dnsCmd.AddCommand(dnsWriteCmd) dnsCmd.AddCommand(dnsWriteCmd)
dnsCmd.AddCommand(dnsSyncCmd)
} }
+1 -1
View File
@@ -37,5 +37,5 @@ func setVerbosity(_ *cobra.Command, _ []string) {
if desired > 6 { if desired > 6 {
desired = 6 desired = 6
} }
log = log.WithLevel(slog.LogLevel(desired)) log = zerolog.New(nil, slog.LogLevel(desired))
} }
+3 -3
View File
@@ -2,8 +2,6 @@ module git.jpi.io/amery/jpictl
go 1.19 go 1.19
replace asciigoat.org/ini => ../../../asciigoat.org/ini
require ( require (
asciigoat.org/ini v0.2.5 asciigoat.org/ini v0.2.5
darvaza.org/core v0.9.8 darvaza.org/core v0.9.8
@@ -18,6 +16,8 @@ require (
github.com/mgechev/revive v1.3.3 github.com/mgechev/revive v1.3.3
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
golang.org/x/crypto v0.12.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 gopkg.in/yaml.v3 v3.0.1
) )
@@ -42,8 +42,8 @@ require (
github.com/rs/zerolog v1.30.0 // indirect github.com/rs/zerolog v1.30.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/mod v0.12.0 // indirect golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.12.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 h1:hgDDz4ecm3ZvehX++m8A/IzAt+B5oDPiRtxatzfUHPQ=
asciigoat.org/core v0.3.9/go.mod h1:CAaHwyw8MpAq4a1MYtN2dxJrsK+hmIdW50OndaQZYPI= 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 h1:luLxgfUc2pzuusYPo/Z/dC/qr9XZPKpSQw8/kS7zNUM=
darvaza.org/core v0.9.8/go.mod h1:Dbme64naxeshQfxcVJX9ZT7AiGyIY8kldfuELVtf8mw= darvaza.org/core v0.9.8/go.mod h1:Dbme64naxeshQfxcVJX9ZT7AiGyIY8kldfuELVtf8mw=
darvaza.org/resolver v0.5.4 h1:dlSBNV14yYsp7Kg7ipwYOMNsLbrpeXa8Z0HBTa0Ryxs= 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= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+5 -1
View File
@@ -71,7 +71,7 @@ func newCephScanTODO(cfg *ceph.Config) *cephScanTODO {
return todo return todo
} }
func (m *Cluster) scanCephMonitors(_ *ScanOptions) error { func (m *Cluster) scanCephMonitors(opts *ScanOptions) error {
cfg, err := m.GetCephConfig() cfg, err := m.GetCephConfig()
switch { switch {
case os.IsNotExist(err): case os.IsNotExist(err):
@@ -94,6 +94,10 @@ func (m *Cluster) scanCephMonitors(_ *ScanOptions) error {
todo.LogMissing(m.log) todo.LogMissing(m.log)
} }
return m.initCephMonitors(opts)
}
func (m *Cluster) initCephMonitors(_ *ScanOptions) error {
// make sure every zone has one // make sure every zone has one
m.ForEachZone(func(z *Zone) bool { m.ForEachZone(func(z *Zone) bool {
_ = z.GetCephMonitors() _ = z.GetCephMonitors()
+2 -1
View File
@@ -27,7 +27,8 @@ type Cluster struct {
Domain string `json:"domain,omitempty" yaml:"domain,omitempty"` Domain string `json:"domain,omitempty" yaml:"domain,omitempty"`
CephFSID uuid.UUID `json:"ceph_fsid,omitempty" yaml:"ceph_fsid,omitempty"` CephFSID uuid.UUID `json:"ceph_fsid,omitempty" yaml:"ceph_fsid,omitempty"`
Zones []*Zone `json:"zones,omitempty" yaml:"zones,omitempty"` Regions []Region `json:",omitempty" yaml:",omitempty"`
Zones []*Zone `json:",omitempty" yaml:",omitempty"`
} }
// revive:enable:line-length-limit // revive:enable:line-length-limit
+2
View File
@@ -14,6 +14,8 @@ func (m *Cluster) init(opts *ScanOptions) error {
m.scanZoneIDs, m.scanZoneIDs,
m.scanSort, m.scanSort,
m.scanGateways, m.scanGateways,
m.initCephMonitors,
m.initRegions,
} { } {
if err := fn(opts); err != nil { if err := fn(opts); err != nil {
return err return err
+130
View File
@@ -0,0 +1,130 @@
package cluster
var (
_ MachineIterator = (*Region)(nil)
_ ZoneIterator = (*Region)(nil)
)
// Region represents a group of zones geographically related
type Region struct {
m *Cluster
zones []*Zone
Name string
Regions []string `json:",omitempty" yaml:",omitempty"`
}
// 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
}
}
}
func (m *Cluster) initRegions(_ *ScanOptions) error {
regions := make(map[string][]*Zone)
// first regions defined by zones
m.ForEachZone(func(z *Zone) bool {
for _, region := range z.Regions {
regions[region] = append(regions[region], z)
}
return false
})
// bind first level regions and their zones
for name, zones := range regions {
m.syncRegions(name, zones...)
}
// and combine zones to produce larger regions
for i := range m.Regions {
r := &m.Regions[i]
m.finishRegion(r)
}
return nil
}
func (m *Cluster) syncRegions(name string, zones ...*Zone) {
for _, r := range m.Regions {
if r.Name == name {
// found
r.m = m
r.zones = zones
return
}
}
// new
m.Regions = append(m.Regions, Region{
m: m,
zones: zones,
Name: name,
})
}
func (m *Cluster) finishRegion(r *Region) {
if r.m != nil {
// ready
return
}
r.m = m
sub := []string{}
for _, name := range r.Regions {
r2, ok := m.getRegion(name)
if !ok {
m.warn(nil).WithField("region", name).Print("unknown region")
continue
}
sub = append(sub, r2.Name)
r.zones = append(r.zones, r2.zones...)
}
r.Regions = sub
}
func (m *Cluster) getRegion(name string) (*Region, bool) {
for i := range m.Regions {
r := &m.Regions[i]
if name == r.Name {
m.finishRegion(r)
return r, true
}
}
return nil, false
}
+3 -2
View File
@@ -19,8 +19,9 @@ type Zone struct {
zones *Cluster zones *Cluster
logger `json:"-" yaml:"-"` logger `json:"-" yaml:"-"`
ID int ID int
Name string Name string
Regions []string `json:",omitempty" yaml:",omitempty"`
Machines Machines
} }
+35 -27
View File
@@ -1,30 +1,38 @@
// Package dns manages DNS entries for the cluster // Package dns manages DNS entries for the cluster
package dns package dns
//// A Config defines a Region import (
//type Config struct { "fmt"
// // Name is the identifier of this Region "net/netip"
// Name string )
// // Regions are a list of (sub)regions that belong to this Region
// Regions []string // Zone represents a set of hosts with high affinity
// // Zones are a list of Zones that directly belong to this Region type Zone struct {
// Zones []string Name string
//}
// Hosts map[int]*Host
//type Region struct { }
// Name string
//} func (z *Zone) String() string {
// if z == nil {
//type Zone struct { return "undetermined"
// Name string }
// return z.Name
// Machines map[int]*Machine }
//}
// // Host represents a member of the cluster
//type Machine struct { type Host struct {
// ID int zone *Zone
//
// Active bool ID int
// Addrs []netip.Addr Active bool
//} Addrs []netip.Addr
// }
func (p *Host) String() string {
if p == nil {
return "undetermined"
}
return fmt.Sprintf("%s-%v", p.zone, p.ID)
}
-26
View File
@@ -1,26 +0,0 @@
package dns
//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: addrs,
// }
//
// return nil
//}
//
+171 -114
View File
@@ -1,116 +1,173 @@
package dns package dns
// // Manager is a DNS Manager instance import (
// type Manager struct { "context"
// mu sync.Mutex "errors"
// "io/fs"
// domain string "net/netip"
// suffix string "strings"
// zones map[string]*Zone
// "darvaza.org/core"
// p Provider "darvaza.org/slog"
// l slog.Logger "git.jpi.io/amery/jpictl/pkg/cluster"
// } "golang.org/x/net/publicsuffix"
// )
// // ManagerOption configures a Manager
// type ManagerOption func(*Manager) error // Manager is a DNS Manager instance
// type Manager struct {
// func newErrorManagerOption(err error, hint string) ManagerOption { domain string
// return func(*Manager) error { suffix string
// if hint != "" { zones map[string]*Zone
// err = core.Wrap(err, hint) regions map[string][]string
// }
// return err p Provider
// } l slog.Logger
// } }
//
// // WithProvider attaches a libdns Provider to the Manager // ManagerOption configures a Manager
// func WithProvider(p Provider) ManagerOption { type ManagerOption func(*Manager) error
// var err error
// func newErrorManagerOption(err error, hint string) ManagerOption {
// if p == nil { return func(*Manager) error {
// p, err = DefaultDNSProvider() return core.Wrap(err, hint)
// } }
// }
// if err != nil {
// return newErrorManagerOption(err, "WithProvider") // WithProvider attaches a libdns Provider to the Manager
// } func WithProvider(p Provider) ManagerOption {
// var err error
// return func(mgr *Manager) error {
// mgr.p = p if p == nil {
// return nil p, err = DefaultDNSProvider()
// } }
// }
// if err != nil {
// // WithLogger attaches a logger to the Manager return newErrorManagerOption(err, "WithProvider")
// func WithLogger(log slog.Logger) ManagerOption { }
// if log == nil {
// log = cluster.DefaultLogger() return func(mgr *Manager) error {
// } mgr.p = p
// return nil
// return func(mgr *Manager) error { }
// mgr.l = log }
// return nil
// } // WithLogger attaches a logger to the Manager
// } func WithLogger(log slog.Logger) ManagerOption {
// if log == nil {
// func (mgr *Manager) setDefaults() error { log = cluster.DefaultLogger()
// var opts []ManagerOption }
//
// if mgr.p == nil { return func(mgr *Manager) error {
// opts = append(opts, WithProvider(nil)) mgr.l = log
// } return nil
// }
// if mgr.l == nil { }
// opts = append(opts, WithLogger(nil))
// } func (mgr *Manager) setDefaults() error {
// var opts []ManagerOption
// if mgr.domain == "" || mgr.suffix == "" {
// return errors.New("domain not specified") if mgr.l == nil {
// } opts = append(opts, WithLogger(nil))
// }
// mgr.zones = make(map[string]*Zone)
// if mgr.domain == "" || mgr.suffix == "" {
// for _, opt := range opts { return errors.New("domain not specified")
// if err := opt(mgr); err != nil { }
// return err
// } for _, opt := range opts {
// } if err := opt(mgr); err != nil {
// return nil return err
// } }
// }
// // WithDomain specifies where the manager operates return nil
// func WithDomain(domain string) ManagerOption { }
// base, err := publicsuffix.EffectiveTLDPlusOne(domain)
// if err != nil { // WithDomain specifies where the manager operates
// return newErrorManagerOption(err, "publicsuffix") func WithDomain(domain string) ManagerOption {
// } base, err := publicsuffix.EffectiveTLDPlusOne(domain)
// if err != nil {
// suffix := strings.TrimSuffix(domain, base) return newErrorManagerOption(err, "publicsuffix")
// if suffix != "" { }
// suffix = "." + suffix[:len(suffix)-1]
// } suffix := strings.TrimSuffix(domain, base)
// if suffix != "" {
// return func(mgr *Manager) error { suffix = "." + suffix[:len(suffix)-1]
// mgr.domain = base }
// mgr.suffix = suffix
// return nil return func(mgr *Manager) error {
// } mgr.domain = base
// } mgr.suffix = suffix
// return nil
// // NewManager creates a DNS manager with the }
// func NewManager(opts ...ManagerOption) (*Manager, error) { }
// mgr := new(Manager)
// // NewManager creates a DNS manager with the
// for _, opt := range opts { func NewManager(opts ...ManagerOption) (*Manager, error) {
// if err := opt(mgr); err != nil { mgr := &Manager{
// return nil, err zones: make(map[string]*Zone),
// } regions: make(map[string][]string),
// } }
//
// if err := mgr.setDefaults(); err != nil { for _, opt := range opts {
// return nil, err if err := opt(mgr); err != nil {
// } return nil, err
// return mgr, nil }
// } }
//
if err := mgr.setDefaults(); err != nil {
return nil, err
}
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 { type Provider interface {
libdns.RecordGetter libdns.RecordGetter
libdns.RecordDeleter libdns.RecordDeleter
libdns.RecordSetter
libdns.RecordAppender
} }
// DefaultDNSProvider returns a cloudflare DNS provider // DefaultDNSProvider returns a cloudflare DNS provider
+178 -3
View File
@@ -1,6 +1,9 @@
package dns package dns
import ( import (
"bytes"
"fmt"
"io"
"net/netip" "net/netip"
"sort" "sort"
"time" "time"
@@ -9,6 +12,58 @@ import (
"github.com/libdns/libdns" "github.com/libdns/libdns"
) )
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])
})
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 // AddrRecord represents an A or AAAA record
type AddrRecord struct { type AddrRecord struct {
Name string Name string
@@ -17,9 +72,7 @@ type AddrRecord struct {
// Sort sorts the addresses of the record // Sort sorts the addresses of the record
func (rr *AddrRecord) Sort() { func (rr *AddrRecord) Sort() {
sort.Slice(rr.Addr, func(i, j int) bool { SortAddrSlice(rr.Addr)
return rr.Addr[i].Less(rr.Addr[j])
})
} }
// Export converts the record into libdns.Record // Export converts the record into libdns.Record
@@ -36,3 +89,125 @@ func (rr *AddrRecord) Export() []libdns.Record {
return out 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
cache := make(map[string][]netip.Addr)
// zones
for _, z := range mgr.zones {
// hosts
s := mgr.genZoneHostRecords(z)
out = append(out, s...)
// zone alias
addrs := mgr.genZoneAddresses(z)
name := z.Name
out = append(out, AddrRecord{
Name: name + mgr.suffix,
Addr: addrs,
})
// and cache for regions
cache[name] = addrs
}
for _, name := range mgr.genRegionsSorted() {
var addrs []netip.Addr
for _, z := range mgr.regions[name] {
addrs = append(addrs, cache[z]...)
}
rec := AddrRecord{
Name: name + mgr.suffix,
Addr: addrs,
}
rec.Sort()
out = append(out, rec)
}
SortAddrRecords(out)
return out
}
func (*Manager) genZoneAddresses(z *Zone) []netip.Addr {
var out []netip.Addr
for _, p := range z.Hosts {
if p.Active {
out = append(out, p.Addrs...)
}
}
SortAddrSlice(out)
return out
}
func (mgr *Manager) genZoneHostRecords(z *Zone) []AddrRecord {
out := make([]AddrRecord, 0, len(z.Hosts))
for _, p := range z.Hosts {
rec := AddrRecord{
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
}
+60 -170
View File
@@ -1,172 +1,62 @@
package dns package dns
// func (mgr *Manager) WriteTo(w io.Writer) (int64, error) { import (
// var buf bytes.Buffer "bytes"
// "fmt"
// var zoneNames sort.StringSlice "io"
// "net/netip"
// // sort zones )
// for name := range mgr.zones {
// zoneNames = append(zoneNames, name) // WriteTo writes the DNS data for the cluster
// } func (mgr *Manager) WriteTo(w io.Writer) (int64, error) {
// sort.Sort(zoneNames) var buf bytes.Buffer
//
// // records cache := make(map[string][]netip.Addr)
// zones := make(map[string][]libdns.Record)
// // zones
// for _, name := range zoneNames { for _, z := range mgr.zones {
// z := mgr.zones[name] mgr.writeZoneHosts(&buf, z)
//
// if buf.Len() > 0 { // zone alias
// _, _ = buf.WriteRune('\n') addrs := mgr.genZoneAddresses(z)
// } zoneName := z.Name
//
// // servers rr := AddrRecord{
// err := mgr.writeZoneMachinesToBuffer(&buf, name, z) Name: mgr.fqdn(zoneName + mgr.suffix),
// if err != nil { Addr: addrs,
// return 0, err }
// } _, _ = rr.WriteTo(&buf)
//
// // zone alias // and cache for regions
// _, _ = fmt.Fprintf(&buf, "; %s\n", name) cache[zoneName] = addrs
// records := mgr.genAliasRecords(name, true, z) }
// mgr.Sort(records)
// mgr.writeRecordsToBuffer(&buf, "", records...) // regions, sorted
// for _, name := range mgr.genRegionsSorted() {
// zones[name] = records addrs := mgr.genRegionAddressesCached(name, cache)
// }
// mgr.writeRegionAddresses(&buf, name, addrs)
// mgr.writeRegionToBuffer(&buf, zones, "uk", "ssd-lon") }
// mgr.writeRegionToBuffer(&buf, zones, "nl", "ssd-ams")
// mgr.writeRegionToBuffer(&buf, zones, "de", "htz-fsn") return buf.WriteTo(w)
// mgr.writeRegionToBuffer(&buf, zones, "eu", "ssd-ams", "htz-fsn") }
// mgr.writeRegionToBuffer(&buf, zones, "europe", "ssd-ams", "ssd-lon", "htz-fsn")
// mgr.writeRegionToBuffer(&buf, zones, "earth", "ssd-ams", "ssd-lon", "htz-fsn") func (mgr *Manager) writeZoneHosts(w io.Writer, z *Zone) {
// _, _ = fmt.Fprintf(w, ";\n; %s\n;\n", z.Name)
// return buf.WriteTo(w)
// } for _, rr := range mgr.genZoneHostRecords(z) {
// rr.Name = mgr.fqdn(rr.Name)
// func (mgr *Manager) writeRecordsToBuffer(buf *bytes.Buffer, rename string, records ...libdns.Record) { _, _ = rr.WriteTo(w)
// for _, rr := range records { }
// name := core.IIf(rename != "", rename, rr.Name) }
// fqdn := fmt.Sprintf("%s.%s.", name, mgr.domain)
// func (mgr *Manager) writeRegionAddresses(w io.Writer, name string, addrs []netip.Addr) {
// _, _ = fmt.Fprintf(buf, "%s\t%v\tIN\t%s\t%v\n", _, _ = fmt.Fprintf(w, "; %s\n", name)
// fqdn, int(rr.TTL/time.Second), rr.Type, rr.Value)
// } rr := AddrRecord{
// } Name: mgr.fqdn(name + mgr.suffix),
// Addr: addrs,
// func (mgr *Manager) writeRegionToBuffer(buf *bytes.Buffer, cache map[string][]libdns.Record, name string, zones ...string) { }
// var alias string
// _, _ = rr.WriteTo(w)
// _, _ = fmt.Fprintf(buf, "\n; %s\n", name) }
//
// switch {
// case mgr.suffix == "":
// // no suffix
// case strings.HasSuffix(name, mgr.suffix):
// // suffixed
// default:
// // unsuffixed
// alias = name
// name = name + mgr.suffix
// }
//
// records := []libdns.Record{}
// for _, zone := range zones {
// records = append(records, cache[zone]...)
// }
// mgr.Sort(records)
// mgr.writeRecordsToBuffer(buf, alias, records...)
//
// if alias != "" {
// rr := libdns.Record{
// Name: alias,
// TTL: 3600 * time.Second,
// Type: "CNAME",
// Value: name + "." + mgr.domain + ".",
// }
//
// mgr.writeRecordsToBuffer(buf, alias, rr)
// }
// }
//
// func (mgr *Manager) writeZoneMachinesToBuffer(buf *bytes.Buffer, zone_name string, z *Zone) error {
// // title
// _, _ = fmt.Fprintf(buf, ";\n; %s\n;\n", zone_name)
//
// // machine records
// records := mgr.genMachineRecords(z)
// mgr.writeRecordsToBuffer(buf, "", records...)
//
// return nil
// }
//
// func (mgr *Manager) genMachineRecords(z *Zone) []libdns.Record {
// var out []libdns.Record
//
// for _, p := range z.Machines {
// pqdn := fmt.Sprintf("%s-%v%s", z.Name, p.ID, mgr.suffix)
//
// for _, addr := range p.Addrs {
// out = append(out, libdns.Record{
// Name: pqdn,
// Type: core.IIf(addr.Is6(), "AAAA", "A"),
// TTL: time.Second,
// Value: addr.String(),
// })
// }
// }
//
// mgr.Sort(out)
// return out
// }
//
// func (mgr *Manager) genAliasRecords(name string, activeOnly bool, zones ...*Zone) []libdns.Record {
// var out []libdns.Record
//
// fqdn := fmt.Sprintf("%s%s", name, mgr.suffix)
// for _, z := range zones {
// for _, p := range z.Machines {
// if p.Active || !activeOnly {
// for _, addr := range p.Addrs {
// out = append(out, libdns.Record{
// Name: fqdn,
// Type: core.IIf(addr.Is6(), "AAAA", "A"),
// TTL: time.Second,
// Value: addr.String(),
// })
// }
// }
// }
// }
//
// return out
// }
//
// func (mgr *Manager) Sort(records []libdns.Record) {
// sort.SliceStable(records, func(i, j int) bool {
// a := &records[i]
// b := &records[j]
// switch {
// case a.Name < b.Name:
// return true
// case a.Name > b.Name:
// return false
// case a.Type < b.Type:
// return true
// case a.Type > b.Type:
// return false
// case a.Type == "A" || a.Type == "AAAA":
// var aAddr, bAddr netip.Addr
// aAddr.UnmarshalText([]byte(a.Value))
// bAddr.UnmarshalText([]byte(b.Value))
// return aAddr.Less(bAddr)
// case a.Value < b.Value:
// return true
// default:
// return false
// }
// })
// }
//
+110 -14
View File
@@ -2,6 +2,7 @@ package wireguard
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"net/netip" "net/netip"
@@ -9,8 +10,8 @@ import (
"strings" "strings"
"text/template" "text/template"
"asciigoat.org/ini/basic"
"darvaza.org/core" "darvaza.org/core"
"gopkg.in/gcfg.v1"
) )
var configTemplate = template.Must(template.New("config").Funcs(template.FuncMap{ 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" // FromString sets the EndpointAddress from a given "[host]:port"
func (ep *EndpointAddress) FromString(s string) error { func (ep *EndpointAddress) FromString(s string) error {
host, port, err := core.SplitHostPort(s) host, port, err := core.SplitHostPort(s)
@@ -131,6 +127,98 @@ func (ep *EndpointAddress) FromString(s string) error {
return nil 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) { func parseAllowedIPs(data string) ([]netip.Prefix, error) {
var out []netip.Prefix var out []netip.Prefix
@@ -147,17 +235,25 @@ func parseAllowedIPs(data string) ([]netip.Prefix, error) {
return out, nil 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 // NewConfigFromReader parses a wgN.conf file
func NewConfigFromReader(r io.Reader) (*Config, error) { func NewConfigFromReader(r io.Reader) (*Config, error) {
doc, err := basic.Decode(r) temp := &intermediateConfig{}
if err != nil {
if err := gcfg.ReadInto(temp, r); err != nil {
return nil, err return nil, err
} }
cfg, err := newConfigFromDocument(doc) return temp.Export()
if err != nil {
return nil, err
}
return cfg, nil
} }
-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. // MarshalJSON encodes the key for JSON, omitting empty.
func (key PrivateKey) MarshalJSON() ([]byte, error) { func (key PrivateKey) MarshalJSON() ([]byte, error) {
return encodeKeyJSON(key.String()) return encodeKeyJSON(key.String())