Compare commits

..

52 Commits

Author SHA1 Message Date
amery 2207e4a4a4 zones: fix New() to handle relative paths on hackpadfs
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-25 16:02:00 +00:00
amery 7ca01aa1e4 zones: Machine.RemoveWireguardConfig()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-25 15:36:58 +00:00
amery 8b72667f4d zones: Machine.RemoveWireguardKeys()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-25 15:20:07 +00:00
amery 49694eb7cb zones: Machine.WriteWireguardKeys()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-25 14:53:31 +00:00
amery 15a98c05ec zones: Machine.WriteStringFile()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-25 14:53:24 +00:00
amery a005823d44 zones: Machine.CreateFile() and Machine.CreateTruncFile()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-25 14:53:24 +00:00
amery 7af8484acc zones: introduce Machine.OpenFile()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 21:14:33 +00:00
amery 0f1f1ce968 zones: introduce Machine.RemoveFile()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 21:14:33 +00:00
amery 5058f286c6 zones: switch to using hackpadfs/os.FS as the standard os.FS is incomplete
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 21:14:33 +00:00
amery 86075eb47f zones: move Machine.ReadFile to a dedicated machine_file.go
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 21:14:31 +00:00
amery c81b782b26 zones: Machine.IsGateway()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 21:07:55 +00:00
amery 0f62ee2e53 zones: rename Machine.RingAddresses to Machine.Rings
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 20:52:08 +00:00
amery 30a7bceda3 wireguard: make KeyPairs solid
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 19:49:59 +00:00
amery 60e2687d04 wireguard: make keys arrays instead of slices
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 19:35:11 +00:00
amery 1419e55d5b zones: remove useless RingInfo.Address
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 19:06:33 +00:00
amery ffdacb833b zones: add Port information to RingAddressEncoder
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 17:56:19 +00:00
amery aca0a5e834 zones: calculate Machine.ID on init
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 17:55:43 +00:00
amery 61374d4cc5 zones: load wireguard key pairs on Machine.init()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 14:35:40 +00:00
amery 975e166da7 zones: allow RingInfo.Merge() to enable, but not disable
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-24 14:34:00 +00:00
amery b16c648f2c zones: introduce Machine.GetWireguardKeys()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-23 21:03:03 +00:00
amery 47d79f7576 wireguard: introduce KeyPair.Validate()
it will also set the PublicKey field is empty

Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-23 00:29:15 +00:00
amery e2f831fd6a wireguard: introduce NewKeyPair, NewPrivateKey, and PrivateKey.Public()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-23 00:18:31 +00:00
amery 1d8c818ec4 wireguard: make PrivateKey and PublicKey two distinct types
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 23:46:24 +00:00
amery 2f51a463b2 zones: reduce writeEnvZone() complexity
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 22:53:13 +00:00
amery 0c0cba6fb5 jpictl: introduce env command
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 22:27:11 +00:00
amery 75206e4fa5 zones: Zones.WriteEnv() writing env variables describing the cluster
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 22:26:48 +00:00
amery b084e103b9 zones: introduce Machine.getRingInfo()
and refactor Machine.applyRingInfo()

Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 22:25:11 +00:00
amery 223edf846b zones: introduce Zone.ForEachMachine()
and refactor Zones.ForEachMachine() using it

Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 22:25:09 +00:00
amery fdb0f0324f zones: finish scan sorting the content
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 21:29:12 +00:00
amery 9aef92f32d zones: assign zoneID to zones inferable ID
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 21:02:21 +00:00
amery e5baf53758 zones: import wireguard keys from wgN.conf files
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 20:37:10 +00:00
amery 0fe451eed0 zones: introduce RingInfo.Merge()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 20:32:29 +00:00
amery cb5ea80e66 zones: introduce Zones.GetMachineByName()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 20:32:26 +00:00
amery f7da9519fa zones: allow iterators to terminate
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 20:12:05 +00:00
amery 589fb2f0e1 zones: set zoneID based on wireguard config files
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 19:39:30 +00:00
amery f5ee63e5aa wireguard: cleanup Config parser using BinaryKey
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 19:39:28 +00:00
amery 0de2e3f4d9 wireguard: introduce BinaryKey.Equal()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 19:39:26 +00:00
amery c92873f07d zones: introduce RingInfo and Machine.Rings
RingInfo includes Wireguard ring related details

Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 17:11:19 +00:00
amery 4d25ea1d16 wireguard: introduce initial BinaryKey and KeyPair structs
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 17:11:18 +00:00
amery 0d14510958 zones: introduce RingAddressEncoder and RingZero/RingOne implementations
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 15:57:10 +00:00
amery c3b47ba812 zones: introduce ValidZoneID() and ValidNodeID()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 15:43:42 +00:00
amery 15f5aab449 zones: rename address.go to rings.go
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 15:43:40 +00:00
amery 7cf3ee04f5 zones: use cloudflare's resolver
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 03:31:12 +00:00
amery a3e3cde4c4 zones: fix scanMachines to run after all zones have been read
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 02:54:35 +00:00
amery a4a10d0226 zones: add helpers to compose and parse wg0/wg1 addresses
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 02:50:54 +00:00
amery 06e755ecd2 zones: scan for Machine details after they've all been added
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 02:01:00 +00:00
amery b15f394199 wireguard: add EndpointAddress.Name() removing the domain from Host
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 01:51:15 +00:00
amery f225e15b2c wireguard: rename EndpointAddress.Name to Host
and add String() method

Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 01:51:14 +00:00
amery 5d946e4e93 wireguard: adds wgN.conf parser
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 01:35:35 +00:00
amery 979324f151 zones: adds ForEachZone iterator
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 00:38:41 +00:00
amery be9da490ff zones: adds ForEachMachine() iterator
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 00:38:38 +00:00
amery 3599812072 zones: introduce Machine.ReadFile()
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-08-22 00:38:38 +00:00
14 changed files with 1284 additions and 33 deletions
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"os"
"github.com/spf13/cobra"
"git.jpi.io/amery/jpictl/pkg/zones"
)
// Command
var envCmd = &cobra.Command{
Use: "env",
Short: "generates environment variables for shell scripts",
RunE: func(_ *cobra.Command, _ []string) error {
m, err := zones.New(cfg.Base, cfg.Domain)
if err != nil {
return err
}
return m.WriteEnv(os.Stdout)
},
}
func init() {
rootCmd.AddCommand(envCmd)
}
+5 -1
View File
@@ -3,16 +3,19 @@ module git.jpi.io/amery/jpictl
go 1.19
require (
darvaza.org/core v0.9.5
darvaza.org/resolver v0.5.2
darvaza.org/sidecar v0.0.0-20230721122716-b9c54b8adbaf
darvaza.org/slog v0.5.2
github.com/burntSushi/toml v0.3.1
github.com/hack-pad/hackpadfs v0.2.1
github.com/mgechev/revive v1.3.2
github.com/spf13/cobra v1.7.0
golang.org/x/crypto v0.12.0
gopkg.in/gcfg.v1 v1.2.3
)
require (
darvaza.org/core v0.9.5 // indirect
darvaza.org/slog/handlers/filter v0.4.4 // indirect
darvaza.org/slog/handlers/zerolog v0.4.4 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
@@ -36,4 +39,5 @@ require (
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/tools v0.12.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
+8
View File
@@ -26,6 +26,8 @@ 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/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
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/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -70,6 +72,8 @@ 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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -87,6 +91,10 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/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=
+219
View File
@@ -0,0 +1,219 @@
package wireguard
import (
"errors"
"fmt"
"io"
"net/netip"
"strconv"
"strings"
"darvaza.org/core"
"gopkg.in/gcfg.v1"
)
// Config represents a wgN.conf file
type Config struct {
Interface InterfaceConfig
Peer []PeerConfig
}
// GetAddress is a shortcut to the interface's address
func (f *Config) GetAddress() netip.Addr {
return f.Interface.Address
}
// Peers tells how many peers are described
func (f *Config) Peers() int {
return len(f.Peer)
}
// InterfaceConfig represents the [Interface] section
type InterfaceConfig struct {
Address netip.Addr
PrivateKey PrivateKey
ListenPort uint16
}
// PeerConfig represents a [Peer] section
type PeerConfig struct {
PublicKey PublicKey
Endpoint EndpointAddress
AllowedIPs []netip.Prefix
}
// EndpointAddress is a host:port pair to reach the Peer
type EndpointAddress struct {
Host string
Port uint16
}
// Name returns the first part of a hostname
func (ep EndpointAddress) Name() string {
before, _, _ := strings.Cut(ep.Host, ".")
return before
}
func (ep EndpointAddress) String() string {
switch {
case ep.Host == "":
return ""
case ep.Port == 0:
return ep.Host
case !strings.ContainsRune(ep.Host, ':'):
return fmt.Sprintf("%s:%v", ep.Host, ep.Port)
default:
return fmt.Sprintf("[%s]:%v", ep.Host, ep.Port)
}
}
// FromString sets the EndpointAddress from a given "[host]:port"
func (ep *EndpointAddress) FromString(s string) error {
host, port, err := core.SplitHostPort(s)
if err != nil {
return err
}
ep.Host = host
switch {
case port != "":
n, _ := strconv.ParseUint(port, 10, 16)
ep.Port = uint16(n)
default:
ep.Port = 0
}
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
for _, s := range strings.Split(data, ",") {
s = strings.TrimSpace(s)
p, err := netip.ParsePrefix(s)
if err != nil {
return out, err
}
out = append(out, p)
}
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) {
temp := &intermediateConfig{}
if err := gcfg.ReadInto(temp, r); err != nil {
return nil, err
}
return temp.Export()
}
+181
View File
@@ -0,0 +1,181 @@
package wireguard
import (
"bytes"
"crypto/rand"
"encoding/base64"
"errors"
"golang.org/x/crypto/curve25519"
)
const (
// PrivateKeySize is the length in bytes of a Wireguard Private Key
PrivateKeySize = 32
// PublicKeySize is the length in bytes of a Wireguard Public Key
PublicKeySize = 32
)
var (
// ErrInvalidKeySize indicates the key size is wrong
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
func (key PrivateKey) IsZero() bool {
var zero PrivateKey
return key.Equal(zero)
}
// IsZero tells if the key hasn't been set
func (pub PublicKey) IsZero() bool {
var zero PublicKey
return pub.Equal(zero)
}
// Equal checks if two private keys are identical
func (key PrivateKey) Equal(alter PrivateKey) bool {
return bytes.Equal(key[:], alter[:])
}
// 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)
switch {
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
type KeyPair struct {
PrivateKey PrivateKey
PublicKey PublicKey
}
// 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
}
+2
View File
@@ -0,0 +1,2 @@
// Package wireguard deals with wireguard config
package wireguard
+102
View File
@@ -0,0 +1,102 @@
package zones
import (
"bytes"
"fmt"
"io"
"strings"
)
// WriteEnv generates environment variables for shell scripts
func (m *Zones) WriteEnv(w io.Writer) error {
var buf bytes.Buffer
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
}
}
+14 -28
View File
@@ -2,47 +2,23 @@ package zones
import (
"net/netip"
"strconv"
"strings"
"sync"
)
// A Machine is a machine on a Zone
type Machine struct {
mu sync.Mutex
zone *Zone
id int
Name string
ID int
Name string `toml:"name"`
PublicAddresses []netip.Addr
PublicAddresses []netip.Addr `toml:"public,omitempty"`
Rings []*RingInfo `toml:"rings,omitempty"`
}
func (m *Machine) String() string {
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
func (m *Machine) FullName() string {
if domain := m.zone.zones.domain; domain != "" {
@@ -56,3 +32,13 @@ func (m *Machine) FullName() string {
return m.Name
}
// IsGateway tells if the Machine is a ring0 gateway
func (m *Machine) IsGateway() bool {
_, ok := m.getRingInfo(0)
return ok
}
func (m *Machine) getPeerByName(name string) (*Machine, bool) {
return m.zone.zones.GetMachineByName(name)
}
+87
View File
@@ -0,0 +1,87 @@
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...)
}
+263
View File
@@ -0,0 +1,263 @@
package zones
import (
"bytes"
"fmt"
"io/fs"
"os"
"darvaza.org/core"
"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
}
// RemoveWireguardKeys deletes wgN.key and wgN.pub from
// the machine's config directory
func (m *Machine) RemoveWireguardKeys(ring int) error {
var err error
err = m.RemoveFile("wg%v.pub", ring)
switch {
case os.IsNotExist(err):
// ignore
case err != nil:
return err
}
err = m.RemoveFile("wg%v.key", ring)
if os.IsNotExist(err) {
// ignore
err = nil
}
return err
}
// GetWireguardConfig reads a wgN.conf file
func (m *Machine) GetWireguardConfig(ring int) (*wireguard.Config, error) {
data, err := m.ReadFile("wg%v.conf", ring)
if err != nil {
return nil, err
}
r := bytes.NewReader(data)
return wireguard.NewConfigFromReader(r)
}
func (m *Machine) tryApplyWireguardConfig(ring int) error {
wg, err := m.GetWireguardConfig(ring)
switch {
case os.IsNotExist(err):
return nil
case err != nil:
return err
default:
return m.applyWireguardConfig(ring, wg)
}
}
func (m *Machine) applyWireguardConfig(ring int, wg *wireguard.Config) error {
addr := wg.GetAddress()
zoneID, nodeID, ok := Rings[ring].Decode(addr)
if !ok {
return fmt.Errorf("%s: invalid wg%v address: %s", m.Name, ring, addr)
}
if err := m.applyZoneNodeID(zoneID, nodeID); err != nil {
err = core.Wrapf(err, "%s: wg%v:%s", m.Name, ring, addr)
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
}
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 {
switch {
case zoneID == 0:
return fmt.Errorf("invalid %s", "zoneID")
case nodeID == 0:
return fmt.Errorf("invalid %s", "nodeID")
case m.ID != nodeID:
return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.ID, nodeID)
case m.zone.ID != 0 && m.zone.ID != zoneID:
return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.zone.ID, zoneID)
case m.zone.ID == 0:
m.zone.ID = zoneID
}
return nil
}
// RemoveWireguardConfig deletes wgN.conf from the machine's
// config directory.
func (m *Machine) RemoveWireguardConfig(ring int) error {
err := m.RemoveFile("wg%v.conf", ring)
if os.IsNotExist(err) {
err = nil
}
return err
}
+37
View File
@@ -3,6 +3,7 @@ package zones
import (
"context"
"net/netip"
"strconv"
"time"
)
@@ -24,3 +25,39 @@ func (m *Machine) updatePublicAddresses() error {
m.PublicAddresses = addrs
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 {
for i := 0; i < RingsCount; i++ {
if err := m.tryApplyWireguardConfig(i); err != nil {
return err
}
}
return m.updatePublicAddresses()
}
+181
View File
@@ -0,0 +1,181 @@
package zones
import (
"fmt"
"net/netip"
"git.jpi.io/amery/jpictl/pkg/wireguard"
)
const (
// MaxZoneID indicates the highest ID allowed for a Zone
MaxZoneID = 0xf
// MaxNodeID indicates the highest Machine ID allowed within a Zone
MaxNodeID = 0xff - 1
// RingsCount indicates how many wireguard rings we have
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
// for a Machine on a particular ring
type RingInfo struct {
Ring int `toml:"ring"`
Enabled bool `toml:"enabled,omitempty"`
Keys wireguard.KeyPair `toml:"keys,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
// Wireguard ring
type RingAddressEncoder struct {
ID int
Port uint16
Encode func(zoneID, nodeID int) (netip.Addr, bool)
Decode func(addr netip.Addr) (zoneID, nodeID int, ok bool)
}
var (
// RingZero is a wg0 address encoder/decoder
RingZero = RingAddressEncoder{
ID: 0,
Port: RingZeroPort,
Decode: ParseRingZeroAddress,
Encode: RingZeroAddress,
}
// RingOne is a wg1 address encoder/decoder
RingOne = RingAddressEncoder{
ID: 1,
Port: RingOnePort,
Decode: ParseRingOneAddress,
Encode: RingOneAddress,
}
// Rings provides indexed access to the ring address encoders
Rings = [RingsCount]RingAddressEncoder{
RingZero,
RingOne,
}
)
// ValidZoneID checks if the given zoneID is a valid 4 bit zone number.
//
// 0 is reserved, and only allowed when composing CIDRs.
func ValidZoneID(zoneID int) bool {
switch {
case zoneID < 0 || zoneID > MaxZoneID:
return false
default:
return true
}
}
// ValidNodeID checks if the given nodeID is a valid 8 bit number.
// nodeID is unique within a Zone.
// 0 is reserved, and only allowed when composing CIDRs.
func ValidNodeID(nodeID int) bool {
switch {
case nodeID < 0 || nodeID > MaxNodeID:
return false
default:
return true
}
}
// ParseRingZeroAddress extracts zone and node ID from a wg0 [netip.Addr]
// wg0 addresses are of the form `10.0.{{zoneID}}.{{nodeID}}`
func ParseRingZeroAddress(addr netip.Addr) (zoneID int, nodeID int, ok bool) {
if addr.IsValid() {
a4 := addr.As4()
if a4[0] == 10 && a4[1] == 0 {
return int(a4[2]), int(a4[3]), true
}
}
return 0, 0, false
}
// RingZeroAddress returns a wg0 IP address
func RingZeroAddress(zoneID, nodeID int) (netip.Addr, bool) {
switch {
case !ValidZoneID(zoneID) || !ValidNodeID(nodeID):
return netip.Addr{}, false
default:
a4 := [4]uint8{10, 0, uint8(zoneID), uint8(nodeID)}
return netip.AddrFrom4(a4), true
}
}
// ParseRingOneAddress extracts zone and node ID from a wg1 [netip.Addr]
// wg1 addresses are of the form `10.{{zoneID << 4}}.{{nodeID}}`
func ParseRingOneAddress(addr netip.Addr) (zoneID int, nodeID int, ok bool) {
if addr.IsValid() {
a4 := addr.As4()
if a4[0] == 10 && a4[2] == 0 {
zoneID = int(a4[1] >> 4)
nodeID = int(a4[3])
return zoneID, nodeID, true
}
}
return 0, 0, false
}
// RingOneAddress returns a wg1 IP address
func RingOneAddress(zoneID, nodeID int) (netip.Addr, bool) {
switch {
case !ValidZoneID(zoneID) || !ValidNodeID(nodeID):
return netip.Addr{}, false
default:
a4 := [4]uint8{10, 0, uint8(zoneID << 4), uint8(nodeID)}
return netip.AddrFrom4(a4), true
}
}
+87 -1
View File
@@ -2,9 +2,25 @@ package zones
import (
"io/fs"
"sort"
)
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
entries, err := fs.ReadDir(m.dir, ".")
if err != nil {
@@ -29,6 +45,76 @@ func (m *Zones) scan() error {
return nil
}
func (m *Zones) scanMachines() error {
var err error
m.ForEachMachine(func(p *Machine) bool {
err = p.scan()
return err != nil
})
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 {
// each directory is a machine
entries, err := fs.ReadDir(z.zones.dir, z.Name)
@@ -43,7 +129,7 @@ func (z *Zone) scan() error {
Name: e.Name(),
}
if err := m.updatePublicAddresses(); err != nil {
if err := m.init(); err != nil {
return err
}
+71 -3
View File
@@ -3,7 +3,9 @@ package zones
import (
"io/fs"
"os"
"path/filepath"
"github.com/hack-pad/hackpadfs/os"
"darvaza.org/resolver"
)
@@ -22,6 +24,16 @@ func (z *Zone) String() string {
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
type Zones struct {
dir fs.FS
@@ -31,11 +43,57 @@ type Zones struct {
Zones []*Zone `toml:"zones"`
}
// ForEachMachine calls a function for each Machine in the cluster
// until instructed to terminate the loop
func (m *Zones) ForEachMachine(fn func(*Machine) bool) {
m.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 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
// name on any zone
func (m *Zones) GetMachineByName(name string) (*Machine, bool) {
var out *Machine
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
func NewFS(dir fs.FS, domain string) (*Zones, error) {
lockuper := resolver.NewCloudflareLookuper()
z := &Zones{
dir: dir,
resolver: resolver.SystemResolver(true),
resolver: resolver.NewResolver(lockuper),
domain: domain,
}
@@ -48,5 +106,15 @@ func NewFS(dir fs.FS, domain string) (*Zones, error) {
// New builds a [Zones] tree using the given directory
func New(dir, domain string) (*Zones, error) {
return NewFS(os.DirFS(dir), domain)
dir, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
base, err := os.NewFS().Sub(dir[1:])
if err != nil {
return nil, err
}
return NewFS(base, domain)
}