Compare commits

..

2 Commits

Author SHA1 Message Date
amery 29dfde5e46 jpictl: initial env command [WIP]
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 19:39:55 +00:00
amery 93aac4c04c zones: WriteEnv() [WIP]
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 19:39:55 +00:00
12 changed files with 92 additions and 752 deletions
-2
View File
@@ -8,10 +8,8 @@ require (
darvaza.org/sidecar v0.0.0-20230721122716-b9c54b8adbaf darvaza.org/sidecar v0.0.0-20230721122716-b9c54b8adbaf
darvaza.org/slog v0.5.2 darvaza.org/slog v0.5.2
github.com/burntSushi/toml v0.3.1 github.com/burntSushi/toml v0.3.1
github.com/hack-pad/hackpadfs v0.2.1
github.com/mgechev/revive v1.3.2 github.com/mgechev/revive v1.3.2
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
golang.org/x/crypto v0.12.0
gopkg.in/gcfg.v1 v1.2.3 gopkg.in/gcfg.v1 v1.2.3
) )
-4
View File
@@ -26,8 +26,6 @@ github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBD
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/hack-pad/hackpadfs v0.2.1 h1:FelFhIhv26gyjujoA/yeFO+6YGlqzmc9la/6iKMIxMw=
github.com/hack-pad/hackpadfs v0.2.1/go.mod h1:khQBuCEwGXWakkmq8ZiFUvUZz84ZkJ2KNwKvChs4OrU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -72,8 +70,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+4 -4
View File
@@ -31,13 +31,13 @@ func (f *Config) Peers() int {
// InterfaceConfig represents the [Interface] section // InterfaceConfig represents the [Interface] section
type InterfaceConfig struct { type InterfaceConfig struct {
Address netip.Addr Address netip.Addr
PrivateKey PrivateKey PrivateKey BinaryKey
ListenPort uint16 ListenPort uint16
} }
// PeerConfig represents a [Peer] section // PeerConfig represents a [Peer] section
type PeerConfig struct { type PeerConfig struct {
PublicKey PublicKey PublicKey BinaryKey
Endpoint EndpointAddress Endpoint EndpointAddress
AllowedIPs []netip.Prefix AllowedIPs []netip.Prefix
} }
@@ -135,7 +135,7 @@ func (p interfaceConfig) Export() (InterfaceConfig, error) {
ListenPort: p.ListenPort, ListenPort: p.ListenPort,
} }
out.PrivateKey, err = PrivateKeyFromBase64(p.PrivateKey) out.PrivateKey, err = BinaryKeyFromBase64(p.PrivateKey)
if err != nil { if err != nil {
err = core.Wrap(err, "PrivateKey") err = core.Wrap(err, "PrivateKey")
return InterfaceConfig{}, err return InterfaceConfig{}, err
@@ -162,7 +162,7 @@ func (v *intermediateConfig) ExportPeer(i int) (PeerConfig, error) {
} }
// PublicKey // PublicKey
out.PublicKey, err = PublicKeyFromBase64(v.Peer.PublicKey[i]) out.PublicKey, err = BinaryKeyFromBase64(v.Peer.PublicKey[i])
if err != nil { if err != nil {
err = core.Wrap(err, "PublicKey") err = core.Wrap(err, "PublicKey")
return out, err return out, err
+15 -160
View File
@@ -2,180 +2,35 @@ package wireguard
import ( import (
"bytes" "bytes"
"crypto/rand"
"encoding/base64" "encoding/base64"
"errors"
"golang.org/x/crypto/curve25519"
) )
const ( // BinaryKey is a binary blob
// PrivateKeySize is the length in bytes of a Wireguard Private Key type BinaryKey []byte
PrivateKeySize = 32
// PublicKeySize is the length in bytes of a Wireguard Public Key
PublicKeySize = 32
)
var ( func (k BinaryKey) String() string {
// ErrInvalidKeySize indicates the key size is wrong return base64.StdEncoding.EncodeToString(k)
ErrInvalidKeySize = errors.New("invalid key size")
// ErrInvalidPrivateKey indicates the private key is invalid
ErrInvalidPrivateKey = errors.New("invalid private key")
// ErrInvalidPublicKey indicates the public key is invalid
ErrInvalidPublicKey = errors.New("invalid public key")
)
type (
// PrivateKey is a binary Wireguard Private Key
PrivateKey [PrivateKeySize]byte
// PublicKey is a binary Wireguard Public Key
PublicKey [PublicKeySize]byte
)
func (key PrivateKey) String() string {
switch {
case key.IsZero():
return ""
default:
return base64.StdEncoding.EncodeToString(key[:])
}
}
func (pub PublicKey) String() string {
switch {
case pub.IsZero():
return ""
default:
return base64.StdEncoding.EncodeToString(pub[:])
}
} }
// IsZero tells if the key hasn't been set // IsZero tells if the key hasn't been set
func (key PrivateKey) IsZero() bool { func (k BinaryKey) IsZero() bool {
var zero PrivateKey return len(k) == 0
return key.Equal(zero)
} }
// IsZero tells if the key hasn't been set // Equal checks if two keys are identical
func (pub PublicKey) IsZero() bool { func (k BinaryKey) Equal(alter BinaryKey) bool {
var zero PublicKey return bytes.Equal(k, alter)
return pub.Equal(zero)
} }
// Equal checks if two private keys are identical // BinaryKeyFromBase64 decodes a base64-based string into
func (key PrivateKey) Equal(alter PrivateKey) bool { // a [BinaryKey]
return bytes.Equal(key[:], alter[:]) func BinaryKeyFromBase64(data string) (BinaryKey, error) {
}
// Equal checks if two public keys are identical
func (pub PublicKey) Equal(alter PublicKey) bool {
return bytes.Equal(pub[:], alter[:])
}
// PrivateKeyFromBase64 decodes a base64-based string into
// a [PrivateKey]
func PrivateKeyFromBase64(data string) (PrivateKey, error) {
b, err := decodeKey(data, PrivateKeySize)
if err != nil {
var zero PrivateKey
return zero, err
}
return *(*[PrivateKeySize]byte)(b), nil
}
// PublicKeyFromBase64 decodes a base64-based string into
// a [PublicKey]
func PublicKeyFromBase64(data string) (PublicKey, error) {
b, err := decodeKey(data, PublicKeySize)
if err != nil {
var zero PublicKey
return zero, err
}
return *(*[PublicKeySize]byte)(b), nil
}
func decodeKey(data string, size int) ([]byte, error) {
b, err := base64.StdEncoding.DecodeString(data) b, err := base64.StdEncoding.DecodeString(data)
switch { return BinaryKey(b), err
case err != nil:
return []byte{}, err
case len(b) != size:
err = ErrInvalidKeySize
return []byte{}, err
default:
return b, nil
}
}
// NewPrivateKey creates a new PrivateKey
func NewPrivateKey() (PrivateKey, error) {
var s [PrivateKeySize]byte
_, err := rand.Read(s[:])
if err != nil {
var zero PrivateKey
return zero, err
}
// apply same clamping as wireguard-go/device/noise-helpers.go
s[0] &= 0xf8
s[31] = (s[31] & 0x7f) | 0x40
return s, nil
}
// Public generates the corresponding PublicKey
func (key PrivateKey) Public() PublicKey {
var pub PublicKey
if !key.IsZero() {
in := (*[PrivateKeySize]byte)(&key)
out := (*[PublicKeySize]byte)(&pub)
curve25519.ScalarBaseMult(out, in)
}
return pub
} }
// KeyPair holds a Key pair // KeyPair holds a Key pair
type KeyPair struct { type KeyPair struct {
PrivateKey PrivateKey PrivateKey BinaryKey
PublicKey PublicKey PublicKey BinaryKey
}
// Validate checks the PublicKey matches the PrivateKey,
// and sets the PublicKey if missing
func (kp *KeyPair) Validate() error {
keyLen := len(kp.PrivateKey)
pubLen := len(kp.PublicKey)
switch {
case keyLen != PrivateKeySize:
// bad private key
return ErrInvalidPrivateKey
case pubLen == 0:
// no public key, set it
kp.PublicKey = kp.PrivateKey.Public()
return nil
case pubLen != PublicKeySize:
// bad public key
return ErrInvalidPublicKey
case !kp.PrivateKey.Public().Equal(kp.PublicKey):
// wrong public key
return ErrInvalidPublicKey
default:
// correct public key
return nil
}
}
// NewKeyPair creates a new KeyPair for Wireguard
func NewKeyPair() (KeyPair, error) {
var out KeyPair
key, err := NewPrivateKey()
if err == nil {
out.PrivateKey = key
out.PublicKey = key.Public()
}
return out, nil
} }
+4 -97
View File
@@ -1,102 +1,9 @@
// Package zones abstracts the cluster zones
package zones package zones
import ( import "io"
"bytes"
"fmt"
"io"
"strings"
)
// WriteEnv generates environment variables for shell scripts // WriteEnv generates environment variables for shell scripts
func (m *Zones) WriteEnv(w io.Writer) error { func (*Zones) WriteEnv(io.Writer) error {
var buf bytes.Buffer return nil
m.writeEnvVarFn(&buf, genEnvZones, "ZONES")
m.ForEachZone(func(z *Zone) bool {
m.writeEnvZone(&buf, z)
return false
})
_, err := buf.WriteTo(w)
return err
}
func (m *Zones) writeEnvZone(w io.Writer, z *Zone) {
zoneID := z.ID
// ZONE{zoneID}
m.writeEnvVar(w, genEnvZoneNodes(z), "ZONE%v", zoneID)
// ZONE{zoneID}_NAME
m.writeEnvVar(w, z.Name, "ZONE%v_%s", zoneID, "NAME")
// ZONE{zoneID}_GW
gatewayID := getRingZeroGatewayID(z)
m.writeEnvVar(w, fmt.Sprintf("%v", gatewayID), "ZONE%v_%s", zoneID, "GW")
// ZONE{zoneID}_IP
ip, _ := RingZeroAddress(zoneID, gatewayID)
m.writeEnvVar(w, ip.String(), "ZONE%v_%s", zoneID, "IP")
}
func (m *Zones) writeEnvVarFn(w io.Writer, fn func(*Zones) string, name string, args ...any) {
var value string
if fn != nil {
value = fn(m)
}
m.writeEnvVar(w, value, name, args...)
}
func (*Zones) writeEnvVar(w io.Writer, value string, name string, args ...any) {
if len(args) > 0 {
name = fmt.Sprintf(name, args...)
}
if name != "" {
value = strings.TrimSpace(value)
_, _ = fmt.Fprintf(w, "%s=%q\n", name, value)
}
}
func genEnvZones(m *Zones) string {
s := make([]string, 0, len(m.Zones))
for _, z := range m.Zones {
s = append(s, fmt.Sprintf("%v", z.ID))
}
return strings.Join(s, " ")
}
func genEnvZoneNodes(z *Zone) string {
s := make([]string, 0, len(z.Machines))
for _, p := range z.Machines {
s = append(s, p.Name)
}
return strings.Join(s, " ")
}
func getRingZeroGatewayID(z *Zone) int {
var firstNodeID, gatewayID int
z.ForEachMachine(func(p *Machine) bool {
if firstNodeID == 0 {
firstNodeID = p.ID
}
if p.IsGateway() {
gatewayID = p.ID
}
return gatewayID != 0
})
switch {
case gatewayID == 0:
return firstNodeID
default:
return gatewayID
}
} }
+48 -8
View File
@@ -1,24 +1,52 @@
package zones package zones
import ( import (
"fmt"
"io/fs"
"net/netip" "net/netip"
"path/filepath"
"strconv"
"strings" "strings"
"sync"
) )
// A Machine is a machine on a Zone // A Machine is a machine on a Zone
type Machine struct { type Machine struct {
mu sync.Mutex
zone *Zone zone *Zone
ID int id int
Name string `toml:"name"` Name string `toml:"name"`
PublicAddresses []netip.Addr `toml:"public,omitempty"` PublicAddresses []netip.Addr `toml:"public,omitempty"`
Rings []*RingInfo `toml:"rings,omitempty"` RingAddresses []*RingInfo `toml:"rings,omitempty"`
} }
func (m *Machine) String() string { func (m *Machine) String() string {
return m.Name return m.Name
} }
// ID return the index within the [Zone] associated to this [Machine]
func (m *Machine) ID() int {
m.mu.Lock()
defer m.mu.Unlock()
if m.id == 0 {
zoneName := m.zone.Name
s := m.Name[len(zoneName)+1:]
id, err := strconv.ParseInt(s, 10, 8)
if err != nil {
panic(err)
}
m.id = int(id)
}
return m.id
}
// FullName returns the Name of the machine including domain name // FullName returns the Name of the machine including domain name
func (m *Machine) FullName() string { func (m *Machine) FullName() string {
if domain := m.zone.zones.domain; domain != "" { if domain := m.zone.zones.domain; domain != "" {
@@ -33,12 +61,24 @@ func (m *Machine) FullName() string {
return m.Name return m.Name
} }
// IsGateway tells if the Machine is a ring0 gateway // ReadFile reads a file from the machine's config directory
func (m *Machine) IsGateway() bool { func (m *Machine) ReadFile(name string, args ...any) ([]byte, error) {
_, ok := m.getRingInfo(0) base := m.zone.zones.dir
return ok fullName := m.getFilename(name, args...)
return fs.ReadFile(base, fullName)
} }
func (m *Machine) getPeerByName(name string) (*Machine, bool) { func (m *Machine) getFilename(name string, args ...any) string {
return m.zone.zones.GetMachineByName(name) if len(args) > 0 {
name = fmt.Sprintf(name, args...)
}
s := []string{
m.zone.Name,
m.Name,
name,
}
return filepath.Join(s...)
} }
-87
View File
@@ -1,87 +0,0 @@
package zones
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
fs "github.com/hack-pad/hackpadfs"
)
// OpenFile opens a file on the machine's config directory with the specified flags
func (m *Machine) OpenFile(name string, flags int, args ...any) (fs.File, error) {
base := m.zone.zones.dir
fullName := m.getFilename(name, args...)
return fs.OpenFile(base, fullName, flags, 0644)
}
// CreateTruncFile creates or truncates a file on the machine's config directory
func (m *Machine) CreateTruncFile(name string, args ...any) (io.WriteCloser, error) {
return m.openWriter(name, os.O_CREATE|os.O_TRUNC, args...)
}
// CreateFile creates a file on the machine's config directory
func (m *Machine) CreateFile(name string, args ...any) (io.WriteCloser, error) {
return m.openWriter(name, os.O_CREATE, args...)
}
func (m *Machine) openWriter(name string, flags int, args ...any) (io.WriteCloser, error) {
f, err := m.OpenFile(name, os.O_WRONLY|flags, args...)
if err != nil {
return nil, err
}
return f.(io.WriteCloser), nil
}
// RemoveFile deletes a file from the machine's config directory
func (m *Machine) RemoveFile(name string, args ...any) error {
base := m.zone.zones.dir
fullName := m.getFilename(name, args...)
err := fs.Remove(base, fullName)
switch {
case os.IsNotExist(err):
return nil
default:
return err
}
}
// ReadFile reads a file from the machine's config directory
func (m *Machine) ReadFile(name string, args ...any) ([]byte, error) {
base := m.zone.zones.dir
fullName := m.getFilename(name, args...)
return fs.ReadFile(base, fullName)
}
// WriteStringFile writes the given content to a file on the machine's config directory
func (m *Machine) WriteStringFile(value string, name string, args ...any) error {
f, err := m.CreateTruncFile(name, args...)
if err != nil {
return err
}
defer f.Close()
buf := bytes.NewBufferString(value)
_, err = buf.WriteTo(f)
return err
}
func (m *Machine) getFilename(name string, args ...any) string {
if len(args) > 0 {
name = fmt.Sprintf(name, args...)
}
s := []string{
m.zone.Name,
m.Name,
name,
}
return filepath.Join(s...)
}
+2 -166
View File
@@ -3,7 +3,6 @@ package zones
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/fs"
"os" "os"
"darvaza.org/core" "darvaza.org/core"
@@ -11,100 +10,6 @@ import (
"git.jpi.io/amery/jpictl/pkg/wireguard" "git.jpi.io/amery/jpictl/pkg/wireguard"
) )
// GetWireguardKeys reads a wgN.key/wgN.pub files
func (m *Machine) GetWireguardKeys(ring int) (wireguard.KeyPair, error) {
var (
data []byte
err error
out wireguard.KeyPair
)
data, err = m.ReadFile("wg%v.key", ring)
if err != nil {
// failed to read
return out, err
}
out.PrivateKey, err = wireguard.PrivateKeyFromBase64(string(data))
if err != nil {
// bad key
err = core.Wrapf(err, "wg%v.key", ring)
return out, err
}
data, err = m.ReadFile("wg%v.pub", ring)
switch {
case os.IsNotExist(err):
// no wgN.pub is fine
case err != nil:
// failed to read
return out, err
default:
// good read
out.PublicKey, err = wireguard.PublicKeyFromBase64(string(data))
if err != nil {
// bad key
err = core.Wrapf(err, "wg%v.pub", ring)
return out, err
}
}
err = out.Validate()
return out, err
}
func (m *Machine) tryReadWireguardKeys(ring int) error {
kp, err := m.GetWireguardKeys(ring)
switch {
case os.IsNotExist(err):
// ignore
return nil
case err != nil:
// something went wrong
return err
default:
// import keys
ri := &RingInfo{
Ring: ring,
Keys: kp,
}
return m.applyRingInfo(ring, ri)
}
}
// WriteWireguardKeys writes the wgN.key/wgN.pub files
func (m *Machine) WriteWireguardKeys(ring int) error {
var err error
var key, pub string
var ri *RingInfo
ri, _ = m.getRingInfo(ring)
if ri != nil {
key = ri.Keys.PrivateKey.String()
pub = ri.Keys.PublicKey.String()
}
switch {
case key == "":
return fs.ErrNotExist
case pub == "":
pub = ri.Keys.PrivateKey.Public().String()
}
err = m.WriteStringFile(key, "wg%v.key", ring)
if err != nil {
return err
}
err = m.WriteStringFile(pub, "wg%v.pub", ring)
if err != nil {
return err
}
return nil
}
// GetWireguardConfig reads a wgN.conf file // GetWireguardConfig reads a wgN.conf file
func (m *Machine) GetWireguardConfig(ring int) (*wireguard.Config, error) { func (m *Machine) GetWireguardConfig(ring int) (*wireguard.Config, error) {
data, err := m.ReadFile("wg%v.conf", ring) data, err := m.ReadFile("wg%v.conf", ring)
@@ -140,86 +45,17 @@ func (m *Machine) applyWireguardConfig(ring int, wg *wireguard.Config) error {
return err return err
} }
if err := m.applyWireguardInterfaceConfig(ring, wg.Interface); err != nil {
err = core.Wrapf(err, "%s: wg%v:%s", m.Name, ring, addr)
return err
}
for _, peer := range wg.Peer {
if err := m.applyWireguardPeerConfig(ring, peer); err != nil {
err = core.Wrapf(err, "%s: wg%v:%s", m.Name, ring, addr)
return err
}
}
return nil return nil
} }
func (m *Machine) getRingInfo(ring int) (*RingInfo, bool) {
for _, ri := range m.Rings {
if ri.Ring == ring {
return ri, ri.Enabled
}
}
return nil, false
}
func (m *Machine) applyRingInfo(ring int, new *RingInfo) error {
cur, _ := m.getRingInfo(ring)
if cur == nil {
// first, append
m.Rings = append(m.Rings, new)
return nil
}
// extra, merge
return cur.Merge(new)
}
func (m *Machine) applyWireguardInterfaceConfig(ring int, data wireguard.InterfaceConfig) error {
ri := &RingInfo{
Ring: ring,
Enabled: true,
Keys: wireguard.KeyPair{
PrivateKey: data.PrivateKey,
},
}
return m.applyRingInfo(ring, ri)
}
func (m *Machine) applyWireguardPeerConfig(ring int, pc wireguard.PeerConfig) error {
peer, found := m.getPeerByName(pc.Endpoint.Name())
switch {
case !found:
// unknown
case ring == 1 && m.zone != peer.zone:
// invalid zone
default:
// apply RingInfo
ri := &RingInfo{
Ring: ring,
Enabled: true,
Keys: wireguard.KeyPair{
PublicKey: pc.PublicKey,
},
}
return peer.applyRingInfo(ring, ri)
}
return fmt.Errorf("%q: invalid peer endpoint", pc.Endpoint.Host)
}
func (m *Machine) applyZoneNodeID(zoneID, nodeID int) error { func (m *Machine) applyZoneNodeID(zoneID, nodeID int) error {
switch { switch {
case zoneID == 0: case zoneID == 0:
return fmt.Errorf("invalid %s", "zoneID") return fmt.Errorf("invalid %s", "zoneID")
case nodeID == 0: case nodeID == 0:
return fmt.Errorf("invalid %s", "nodeID") return fmt.Errorf("invalid %s", "nodeID")
case m.ID != nodeID: case m.ID() != nodeID:
return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.ID, nodeID) return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.ID(), nodeID)
case m.zone.ID != 0 && m.zone.ID != zoneID: case m.zone.ID != 0 && m.zone.ID != zoneID:
return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.zone.ID, zoneID) return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.zone.ID, zoneID)
case m.zone.ID == 0: case m.zone.ID == 0:
-27
View File
@@ -3,7 +3,6 @@ package zones
import ( import (
"context" "context"
"net/netip" "net/netip"
"strconv"
"time" "time"
) )
@@ -26,32 +25,6 @@ func (m *Machine) updatePublicAddresses() error {
return nil return nil
} }
func (m *Machine) init() error {
if err := m.setID(); err != nil {
return err
}
for i := 0; i < RingsCount; i++ {
if err := m.tryReadWireguardKeys(i); err != nil {
return err
}
}
return nil
}
func (m *Machine) setID() error {
zoneName := m.zone.Name
suffix := m.Name[len(zoneName)+1:]
id, err := strconv.ParseInt(suffix, 10, 8)
if err != nil {
return err
}
m.ID = int(id)
return nil
}
func (m *Machine) scan() error { func (m *Machine) scan() error {
for i := 0; i < RingsCount; i++ { for i := 0; i < RingsCount; i++ {
if err := m.tryApplyWireguardConfig(i); err != nil { if err := m.tryApplyWireguardConfig(i); err != nil {
+4 -58
View File
@@ -1,7 +1,6 @@
package zones package zones
import ( import (
"fmt"
"net/netip" "net/netip"
"git.jpi.io/amery/jpictl/pkg/wireguard" "git.jpi.io/amery/jpictl/pkg/wireguard"
@@ -14,72 +13,21 @@ const (
MaxNodeID = 0xff - 1 MaxNodeID = 0xff - 1
// RingsCount indicates how many wireguard rings we have // RingsCount indicates how many wireguard rings we have
RingsCount = 2 RingsCount = 2
// RingZeroPort is the port wireguard uses for ring0
RingZeroPort = 51800
// RingOnePort is the port wireguard uses for ring1
RingOnePort = 51810
) )
// RingInfo contains represents the Wireguard endpoint details // RingInfo contains represents the Wireguard endpoint details
// for a Machine on a particular ring // for a Machine on a particular ring
type RingInfo struct { type RingInfo struct {
Ring int `toml:"ring"` Ring int `toml:"ring"`
Enabled bool `toml:"enabled,omitempty"` Enabled bool `toml:"enabled,omitempty"`
Keys wireguard.KeyPair `toml:"keys,omitempty"` Keys *wireguard.KeyPair `toml:"keys,omitempty"`
} Address netip.Addr `toml:"address,omitempty"`
// Merge attempts to combine two RingInfo structs
func (ri *RingInfo) Merge(alter *RingInfo) error {
switch {
case alter == nil:
return nil
case ri.Ring != alter.Ring:
// different ring
return fmt.Errorf("invalid %s: %v ≠ %v", "ring", ri.Ring, alter.Ring)
case ri.Enabled && !alter.Enabled:
// can't disable via Merge
return fmt.Errorf("invalid %s: %v → %v", "enabled", ri.Enabled, alter.Enabled)
case !canMergeKeyPairs(ri.Keys, alter.Keys):
// incompatible keypairs
return fmt.Errorf("invalid %s: %s ≠ %s", "keys", ri.Keys, alter.Keys)
}
return ri.unsafeMerge(alter)
}
func (ri *RingInfo) unsafeMerge(alter *RingInfo) error {
// enable via Merge
if alter.Enabled {
ri.Enabled = true
}
// fill the gaps on our keypair
if ri.Keys.PrivateKey.IsZero() {
ri.Keys.PrivateKey = alter.Keys.PrivateKey
}
if ri.Keys.PublicKey.IsZero() {
ri.Keys.PublicKey = alter.Keys.PublicKey
}
return nil
}
func canMergeKeyPairs(p1, p2 wireguard.KeyPair) bool {
switch {
case !p1.PrivateKey.IsZero() && !p2.PrivateKey.IsZero() && !p1.PrivateKey.Equal(p2.PrivateKey):
return false
case !p1.PublicKey.IsZero() && !p2.PublicKey.IsZero() && !p1.PublicKey.Equal(p2.PublicKey):
return false
default:
return true
}
} }
// RingAddressEncoder provides encoder/decoder access for a particular // RingAddressEncoder provides encoder/decoder access for a particular
// Wireguard ring // Wireguard ring
type RingAddressEncoder struct { type RingAddressEncoder struct {
ID int ID int
Port uint16
Encode func(zoneID, nodeID int) (netip.Addr, bool) Encode func(zoneID, nodeID int) (netip.Addr, bool)
Decode func(addr netip.Addr) (zoneID, nodeID int, ok bool) Decode func(addr netip.Addr) (zoneID, nodeID int, ok bool)
} }
@@ -88,14 +36,12 @@ var (
// RingZero is a wg0 address encoder/decoder // RingZero is a wg0 address encoder/decoder
RingZero = RingAddressEncoder{ RingZero = RingAddressEncoder{
ID: 0, ID: 0,
Port: RingZeroPort,
Decode: ParseRingZeroAddress, Decode: ParseRingZeroAddress,
Encode: RingZeroAddress, Encode: RingZeroAddress,
} }
// RingOne is a wg1 address encoder/decoder // RingOne is a wg1 address encoder/decoder
RingOne = RingAddressEncoder{ RingOne = RingAddressEncoder{
ID: 1, ID: 1,
Port: RingOnePort,
Decode: ParseRingOneAddress, Decode: ParseRingOneAddress,
Encode: RingOneAddress, Encode: RingOneAddress,
} }
+5 -85
View File
@@ -2,25 +2,9 @@ package zones
import ( import (
"io/fs" "io/fs"
"sort"
) )
func (m *Zones) scan() error { func (m *Zones) scan() error {
for _, fn := range []func() error{
m.scanDirectory,
m.scanMachines,
m.scanZoneIDs,
m.scanSort,
} {
if err := fn(); err != nil {
return err
}
}
return nil
}
func (m *Zones) scanDirectory() error {
// each directory is a zone // each directory is a zone
entries, err := fs.ReadDir(m.dir, ".") entries, err := fs.ReadDir(m.dir, ".")
if err != nil { if err != nil {
@@ -42,79 +26,19 @@ func (m *Zones) scanDirectory() error {
} }
} }
return nil return m.scanMachines()
} }
func (m *Zones) scanMachines() error { func (m *Zones) scanMachines() error {
var err error var err error
m.ForEachMachine(func(p *Machine) bool { m.ForEachMachine(func(p *Machine) {
err = p.scan() if err == nil {
return err != nil err = p.scan()
}
}) })
return err return err
} }
func (m *Zones) scanZoneIDs() error {
var hasMissing bool
var lastZoneID int
m.ForEachZone(func(z *Zone) bool {
switch {
case z.ID == 0:
hasMissing = true
case z.ID > lastZoneID:
lastZoneID = z.ID
}
return false
})
if hasMissing {
next := lastZoneID + 1
m.ForEachZone(func(z *Zone) bool {
if z.ID == 0 {
z.ID, next = next, next+1
}
return false
})
}
return nil
}
func (m *Zones) scanSort() error {
sort.SliceStable(m.Zones, func(i, j int) bool {
id1 := m.Zones[i].ID
id2 := m.Zones[j].ID
return id1 < id2
})
m.ForEachZone(func(z *Zone) bool {
sort.SliceStable(z.Machines, func(i, j int) bool {
id1 := z.Machines[i].ID
id2 := z.Machines[j].ID
return id1 < id2
})
return false
})
m.ForEachMachine(func(p *Machine) bool {
sort.SliceStable(p.Rings, func(i, j int) bool {
ri1 := p.Rings[i]
ri2 := p.Rings[j]
return ri1.Ring < ri2.Ring
})
return false
})
return nil
}
func (z *Zone) scan() error { func (z *Zone) scan() error {
// each directory is a machine // each directory is a machine
entries, err := fs.ReadDir(z.zones.dir, z.Name) entries, err := fs.ReadDir(z.zones.dir, z.Name)
@@ -129,10 +53,6 @@ func (z *Zone) scan() error {
Name: e.Name(), Name: e.Name(),
} }
if err := m.init(); err != nil {
return err
}
z.Machines = append(z.Machines, m) z.Machines = append(z.Machines, m)
} }
} }
+10 -54
View File
@@ -3,8 +3,7 @@ package zones
import ( import (
"io/fs" "io/fs"
"os"
"github.com/hack-pad/hackpadfs/os"
"darvaza.org/resolver" "darvaza.org/resolver"
) )
@@ -23,16 +22,6 @@ func (z *Zone) String() string {
return z.Name return z.Name
} }
// ForEachMachine calls a function for each Machine in the zone
// until instructed to terminate the loop
func (z *Zone) ForEachMachine(fn func(*Machine) bool) {
for _, p := range z.Machines {
if fn(p) {
return
}
}
}
// Zones represents all zones in a cluster // Zones represents all zones in a cluster
type Zones struct { type Zones struct {
dir fs.FS dir fs.FS
@@ -43,47 +32,19 @@ type Zones struct {
} }
// ForEachMachine calls a function for each Machine in the cluster // ForEachMachine calls a function for each Machine in the cluster
// until instructed to terminate the loop func (m *Zones) ForEachMachine(fn func(*Machine)) {
func (m *Zones) ForEachMachine(fn func(*Machine) bool) { for _, z := range m.Zones {
m.ForEachZone(func(z *Zone) bool { for _, p := range z.Machines {
var term bool fn(p)
z.ForEachMachine(func(p *Machine) bool {
term = fn(p)
return term
})
return term
})
}
// ForEachZone calls a function for each Zone in the cluster
// until instructed to terminate the loop
func (m *Zones) ForEachZone(fn func(*Zone) bool) {
for _, p := range m.Zones {
if fn(p) {
// terminate
return
} }
} }
} }
// GetMachineByName looks for a machine with the specified // ForEachZone calls a function for each Zone in the cluster
// name on any zone func (m *Zones) ForEachZone(fn func(*Zone)) {
func (m *Zones) GetMachineByName(name string) (*Machine, bool) { for _, p := range m.Zones {
var out *Machine fn(p)
if name != "" {
m.ForEachMachine(func(p *Machine) bool {
if p.Name == name {
out = p
}
return out != nil
})
} }
return out, out != nil
} }
// NewFS builds a [Zones] tree using the given directory // NewFS builds a [Zones] tree using the given directory
@@ -105,10 +66,5 @@ func NewFS(dir fs.FS, domain string) (*Zones, error) {
// New builds a [Zones] tree using the given directory // New builds a [Zones] tree using the given directory
func New(dir, domain string) (*Zones, error) { func New(dir, domain string) (*Zones, error) {
base, err := os.NewFS().Sub(dir) return NewFS(os.DirFS(dir), domain)
if err != nil {
return nil, err
}
return NewFS(base, domain)
} }