diff --git a/go.mod b/go.mod index 409dc5f..40cc770 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( asciigoat.org/ini v0.2.5 - darvaza.org/core v0.13.1 + darvaza.org/core v0.13.3 darvaza.org/resolver v0.9.2 darvaza.org/sidecar v0.4.0 darvaza.org/slog v0.5.7 diff --git a/go.sum b/go.sum index 748a1f4..2df28d7 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ asciigoat.org/ini v0.2.5 h1:4gRIp9rU+XQt8+HMqZO5R7GavMv9Yl2+N+je6djDIAE= asciigoat.org/ini v0.2.5/go.mod h1:gmXzJ9XFqf1NLk5nQkj04USQ4tMtdRJHNQX6vp3DzjU= darvaza.org/cache/x/simplelru v0.1.8 h1:rvFucut4wKYbsYc994yR3P0M08NqlsvZxr5G4QK82tw= darvaza.org/cache/x/simplelru v0.1.8/go.mod h1:Mv1isOJTcXYK+aK0AvUe+/3KpRTXDsYga6rdTS/upNs= -darvaza.org/core v0.13.1 h1:ZoAfZ3OLnw+t28qMQQxXrDIkETmT2h5gAO6F1XuBpwg= -darvaza.org/core v0.13.1/go.mod h1:47Ydh67KnzjLNu1mzX3r2zpphbxQqEaihMsUq5GflQ4= +darvaza.org/core v0.13.3 h1:DOsidY49WXsWiJulOIxDq578h/3ekgx0trWxbvgv5bc= +darvaza.org/core v0.13.3/go.mod h1:47Ydh67KnzjLNu1mzX3r2zpphbxQqEaihMsUq5GflQ4= darvaza.org/resolver v0.9.2 h1:sUX6LZ1eN5TzJW7L4m7HM+BvwBeWl8dYYDGVSe+AIhk= darvaza.org/resolver v0.9.2/go.mod h1:XWqPhrxoOKNzRuSozOwmE1M6QVqQL28jEdxylnIO8Nw= darvaza.org/sidecar v0.4.0 h1:wHghxzLsiT82WDBBUf34aTqtOvRBg4UbxVIJgKNXRVA= diff --git a/pkg/cluster/addr.go b/pkg/cluster/addr.go new file mode 100644 index 0000000..3037d4c --- /dev/null +++ b/pkg/cluster/addr.go @@ -0,0 +1,43 @@ +package cluster + +import ( + "net/netip" + + "git.jpi.io/amery/jpictl/pkg/rings" +) + +// RingOnePrefix returns the ring 1 subnet of this [Zone]. +func (z *Zone) RingOnePrefix() netip.Prefix { + subnet, err := rings.RingOnePrefix(z.RegionID(), z.ID) + if err != nil { + panic(err) + } + return subnet +} + +// RingOnePrefix returns the ring 1 subnet this [Machine] belongs +// to. +func (m *Machine) RingOnePrefix() netip.Prefix { + return m.zone.RingOnePrefix() +} + +// RingZeroAddress returns the ring 0 address of the [Machine] +// if it can act as gateway. +func (m *Machine) RingZeroAddress() (netip.Addr, bool) { + addr, err := rings.RingZeroAddress(m.Region(), m.Zone(), m.ID) + if err != nil { + return netip.Addr{}, false + } + + return addr, true +} + +// RingOneAddress returns the ring 1 address of the [Machine] +func (m *Machine) RingOneAddress() netip.Addr { + addr, err := rings.RingOneAddress(m.Region(), m.Zone(), m.ID) + if err != nil { + panic(err) + } + + return addr +} diff --git a/pkg/cluster/ceph.go b/pkg/cluster/ceph.go index b91b98b..3d55301 100644 --- a/pkg/cluster/ceph.go +++ b/pkg/cluster/ceph.go @@ -66,7 +66,7 @@ func (m *Cluster) GenCephConfig() (*ceph.Config, error) { m.ForEachZone(func(z *Zone) bool { for _, p := range z.GetCephMonitors() { - addr, _ := RingOneAddress(z.ID, p.ID) + addr := p.RingOneAddress() cfg.Global.Monitors = append(cfg.Global.Monitors, p.Name) cfg.Global.MonitorsAddr = append(cfg.Global.MonitorsAddr, addr) diff --git a/pkg/cluster/ceph_scan.go b/pkg/cluster/ceph_scan.go index bd65821..59e5edd 100644 --- a/pkg/cluster/ceph_scan.go +++ b/pkg/cluster/ceph_scan.go @@ -4,6 +4,7 @@ import ( "os" "darvaza.org/slog" + "git.jpi.io/amery/jpictl/pkg/ceph" ) @@ -14,8 +15,7 @@ type cephScanTODO struct { func (todo *cephScanTODO) checkMachine(p *Machine) bool { // on ceph all addresses are ring1 - ring1, _ := RingOneAddress(p.Zone(), p.ID) - addr := ring1.String() + addr := p.RingOneAddress().String() if _, found := todo.names[p.Name]; found { // found on the TODO by name diff --git a/pkg/cluster/env.go b/pkg/cluster/env.go index 5a6674d..42e98a2 100644 --- a/pkg/cluster/env.go +++ b/pkg/cluster/env.go @@ -185,7 +185,7 @@ func genEnvZoneCephMonNames(m Machines) string { func genEnvZoneCephMonIPs(m Machines) string { var buf strings.Builder m.ForEachMachine(func(p *Machine) bool { - addr, _ := RingOneAddress(p.Zone(), p.ID) + addr := p.RingOneAddress() if buf.Len() > 0 { _, _ = buf.WriteRune(' ') diff --git a/pkg/cluster/errors.go b/pkg/cluster/errors.go index 9035d36..71c133f 100644 --- a/pkg/cluster/errors.go +++ b/pkg/cluster/errors.go @@ -1,6 +1,13 @@ package cluster -import "errors" +import ( + "errors" + "io/fs" + + "darvaza.org/core" + + "git.jpi.io/amery/jpictl/pkg/rings" +) var ( // ErrInvalidName indicates the name isn't valid @@ -14,3 +21,9 @@ var ( // the intended purpose ErrInvalidNode = errors.New("invalid node") ) + +// ErrInvalidRing returns an error indicating the [rings.RingID] +// can't be used for the intended purpose +func ErrInvalidRing(ringID rings.RingID) error { + return core.QuietWrap(fs.ErrInvalid, "invalid ring %v", ringID) +} diff --git a/pkg/cluster/hosts.go b/pkg/cluster/hosts.go index 359aa75..af7b23a 100644 --- a/pkg/cluster/hosts.go +++ b/pkg/cluster/hosts.go @@ -71,14 +71,14 @@ func (p *Machine) WriteHosts() error { func (z *Zone) genHosts(out *hostsFile, p *Machine) { var names []string - ip, _ := RingOneAddress(p.zone.ID, p.ID) + ip := p.RingOneAddress() names = append(names, p.Name) if p.CephMonitor { names = append(names, fmt.Sprintf("%s-%s", p.zone.Name, "ceph")) names = append(names, fmt.Sprintf("%s-%s", p.zone.Name, "k3s")) - if z.ID == p.zone.ID { + if z.Is(p.Region(), p.Zone()) { names = append(names, "ceph") names = append(names, "k3s") } @@ -94,7 +94,7 @@ func (z *Zone) genHosts(out *hostsFile, p *Machine) { if p.IsGateway() { var s string - ip, _ = RingZeroAddress(p.zone.ID, p.ID) + ip, _ = p.RingZeroAddress() s = fmt.Sprintf("%s-%v", p.Name, 0) entry = hostsEntry{ diff --git a/pkg/cluster/machine.go b/pkg/cluster/machine.go index 0d7259a..27ffd59 100644 --- a/pkg/cluster/machine.go +++ b/pkg/cluster/machine.go @@ -53,13 +53,13 @@ func (m *Machine) IsActive() bool { // IsGateway tells if the Machine is a ring0 gateway func (m *Machine) IsGateway() bool { - _, ok := m.getRingInfo(0) + _, ok := m.getRingInfo(rings.RingZeroID) return ok } // SetGateway enables/disables a Machine ring0 integration func (m *Machine) SetGateway(enabled bool) error { - ri, found := m.getRingInfo(0) + ri, found := m.getRingInfo(rings.RingZeroID) switch { case !found && !enabled: return nil @@ -72,7 +72,7 @@ func (m *Machine) SetGateway(enabled bool) error { } ri.Enabled = enabled - return m.SyncWireguardConfig(0) + return m.SyncWireguardConfig(rings.RingZeroID) } // Zone indicates the [Zone] this machine belongs to @@ -80,6 +80,11 @@ func (m *Machine) Zone() rings.ZoneID { return m.zone.ID } +// Region indicates the [Region] this machine belongs to +func (m *Machine) Region() rings.RegionID { + return m.zone.RegionID() +} + func (m *Machine) getPeerByName(name string) (*Machine, bool) { return m.zone.zones.GetMachineByName(name) } diff --git a/pkg/cluster/machine_rings.go b/pkg/cluster/machine_rings.go index 1c0c2f0..8e4d744 100644 --- a/pkg/cluster/machine_rings.go +++ b/pkg/cluster/machine_rings.go @@ -13,14 +13,21 @@ import ( ) // GetWireguardKeys reads a wgN.key/wgN.pub files -func (m *Machine) GetWireguardKeys(ring int) (wireguard.KeyPair, error) { +func (m *Machine) GetWireguardKeys(ringID rings.RingID) (wireguard.KeyPair, error) { var ( data []byte - err error out wireguard.KeyPair ) - data, err = m.ReadFile("wg%v.key", ring) + ring, err := AsWireguardInterfaceID(ringID) + if err != nil { + // invalid ring + return out, err + } + + keyFile, pubFile, _ := ring.Files() + + data, err = m.ReadFile(keyFile) if err != nil { // failed to read return out, err @@ -29,11 +36,11 @@ func (m *Machine) GetWireguardKeys(ring int) (wireguard.KeyPair, error) { out.PrivateKey, err = wireguard.PrivateKeyFromBase64(string(data)) if err != nil { // bad key - err = core.Wrap(err, "wg%v.key", ring) + err = core.Wrap(err, keyFile) return out, err } - data, err = m.ReadFile("wg%v.pub", ring) + data, err = m.ReadFile(pubFile) switch { case os.IsNotExist(err): // no wgN.pub is fine @@ -45,7 +52,7 @@ func (m *Machine) GetWireguardKeys(ring int) (wireguard.KeyPair, error) { out.PublicKey, err = wireguard.PublicKeyFromBase64(string(data)) if err != nil { // bad key - err = core.Wrap(err, "wg%v.pub", ring) + err = core.Wrap(err, pubFile) return out, err } } @@ -54,8 +61,8 @@ func (m *Machine) GetWireguardKeys(ring int) (wireguard.KeyPair, error) { return out, err } -func (m *Machine) tryReadWireguardKeys(ring int) error { - kp, err := m.GetWireguardKeys(ring) +func (m *Machine) tryReadWireguardKeys(ringID rings.RingID) error { + kp, err := m.GetWireguardKeys(ringID) switch { case os.IsNotExist(err): // ignore @@ -66,20 +73,25 @@ func (m *Machine) tryReadWireguardKeys(ring int) error { default: // import keys ri := &RingInfo{ - Ring: ring, + Ring: MustWireguardInterfaceID(ringID), Keys: kp, } - return m.applyRingInfo(ring, ri) + return m.applyRingInfo(ringID, ri) } } // RemoveWireguardKeys deletes wgN.key and wgN.pub from // the machine's config directory -func (m *Machine) RemoveWireguardKeys(ring int) error { - var err error +func (m *Machine) RemoveWireguardKeys(ringID rings.RingID) error { + ring, err := AsWireguardInterfaceID(ringID) + if err != nil { + return err + } + + keyFile, pubFile, _ := ring.Files() - err = m.RemoveFile("wg%v.pub", ring) + err = m.RemoveFile(pubFile) switch { case os.IsNotExist(err): // ignore @@ -87,7 +99,7 @@ func (m *Machine) RemoveWireguardKeys(ring int) error { return err } - err = m.RemoveFile("wg%v.key", ring) + err = m.RemoveFile(keyFile) if os.IsNotExist(err) { // ignore err = nil @@ -97,8 +109,13 @@ func (m *Machine) RemoveWireguardKeys(ring int) error { } // GetWireguardConfig reads a wgN.conf file -func (m *Machine) GetWireguardConfig(ring int) (*wireguard.Config, error) { - data, err := m.ReadFile("wg%v.conf", ring) +func (m *Machine) GetWireguardConfig(ringID rings.RingID) (*wireguard.Config, error) { + ring, err := AsWireguardInterfaceID(ringID) + if err != nil { + return nil, err + } + + data, err := m.ReadFile(ring.ConfFile()) if err != nil { return nil, err } @@ -107,7 +124,7 @@ func (m *Machine) GetWireguardConfig(ring int) (*wireguard.Config, error) { return wireguard.NewConfigFromReader(r) } -func (m *Machine) tryApplyWireguardConfig(ring int) error { +func (m *Machine) tryApplyWireguardConfig(ring rings.RingID) error { wg, err := m.GetWireguardConfig(ring) switch { case os.IsNotExist(err): @@ -119,15 +136,15 @@ func (m *Machine) tryApplyWireguardConfig(ring int) error { } } -func (m *Machine) applyWireguardConfigNode(ring int, wg *wireguard.Config) error { +func (m *Machine) applyWireguardConfigNode(ring rings.RingID, wg *wireguard.Config) error { addr := wg.GetAddress() if !core.IsZero(addr) { - zoneID, nodeID, ok := Rings[ring].Decode(addr) + regionID, zoneID, nodeID, ok := Rings[ring].Decode(addr) if !ok { return fmt.Errorf("%s: invalid address", addr) } - if err := m.applyZoneNodeID(zoneID, nodeID); err != nil { + if err := m.applyZoneNodeID(regionID, zoneID, nodeID); err != nil { return core.Wrap(err, "%s: invalid address", addr) } } @@ -139,7 +156,7 @@ func (m *Machine) applyWireguardConfigNode(ring int, wg *wireguard.Config) error return nil } -func (m *Machine) applyWireguardConfig(ring int, wg *wireguard.Config) error { +func (m *Machine) applyWireguardConfig(ring rings.RingID, wg *wireguard.Config) error { if err := m.applyWireguardConfigNode(ring, wg); err != nil { return err } @@ -153,7 +170,7 @@ func (m *Machine) applyWireguardConfig(ring int, wg *wireguard.Config) error { WithField("subsystem", "wireguard"). WithField("node", m.Name). WithField("peer", peer.Endpoint.Host). - WithField("ring", ring). + WithField("ring", MustWireguardInterfaceID(ring)). Print("ignoring unknown endpoint") case err != nil: return core.Wrap(err, "peer") @@ -163,9 +180,9 @@ func (m *Machine) applyWireguardConfig(ring int, wg *wireguard.Config) error { return nil } -func (m *Machine) getRingInfo(ring int) (*RingInfo, bool) { +func (m *Machine) getRingInfo(ring rings.RingID) (*RingInfo, bool) { for _, ri := range m.Rings { - if ri.Ring == ring { + if ri.RingID() == ring { return ri, ri.Enabled } } @@ -173,13 +190,13 @@ func (m *Machine) getRingInfo(ring int) (*RingInfo, bool) { return nil, false } -func (m *Machine) applyRingInfo(ring int, new *RingInfo) error { +func (m *Machine) applyRingInfo(ring rings.RingID, new *RingInfo) error { cur, _ := m.getRingInfo(ring) if cur == nil { // first, append m.debug(). WithField("node", m.Name). - WithField("ring", ring). + WithField("ring", MustWireguardInterfaceID(ring)). Print("found") m.Rings = append(m.Rings, new) return nil @@ -189,9 +206,11 @@ func (m *Machine) applyRingInfo(ring int, new *RingInfo) error { return cur.Merge(new) } -func (m *Machine) applyWireguardInterfaceConfig(ring int, data wireguard.InterfaceConfig) error { +func (m *Machine) applyWireguardInterfaceConfig(ring rings.RingID, + data wireguard.InterfaceConfig) error { + // ri := &RingInfo{ - Ring: ring, + Ring: MustWireguardInterfaceID(ring), Enabled: true, Keys: wireguard.KeyPair{ PrivateKey: data.PrivateKey, @@ -201,7 +220,9 @@ func (m *Machine) applyWireguardInterfaceConfig(ring int, data wireguard.Interfa return m.applyRingInfo(ring, ri) } -func (m *Machine) applyWireguardPeerConfig(ring int, pc wireguard.PeerConfig) error { +func (m *Machine) applyWireguardPeerConfig(ring rings.RingID, + pc wireguard.PeerConfig) error { + // peer, found := m.getPeerByName(pc.Endpoint.Name()) switch { case !found: @@ -213,7 +234,7 @@ func (m *Machine) applyWireguardPeerConfig(ring int, pc wireguard.PeerConfig) er default: // apply RingInfo ri := &RingInfo{ - Ring: ring, + Ring: MustWireguardInterfaceID(ring), Enabled: true, Keys: wireguard.KeyPair{ PublicKey: pc.PublicKey, @@ -224,21 +245,29 @@ func (m *Machine) applyWireguardPeerConfig(ring int, pc wireguard.PeerConfig) er } } -func (m *Machine) applyZoneNodeID(zoneID rings.ZoneID, nodeID rings.NodeID) error { +func (m *Machine) applyZoneNodeID(regionID rings.RegionID, + zoneID rings.ZoneID, nodeID rings.NodeID) error { + // switch { - case zoneID == 0: + case !regionID.Valid(): + return fmt.Errorf("invalid %s", "regionID") + case !zoneID.Valid(): return fmt.Errorf("invalid %s", "zoneID") - case nodeID == 0: + case !nodeID.Valid(): return fmt.Errorf("invalid %s", "nodeID") case m.ID != nodeID: - return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.ID, nodeID) + return fmt.Errorf("invalid %s: %v ≠ %v", "nodeID", 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 - } + case m.Region() != regionID: + return fmt.Errorf("invalid %s: %v ≠ %v", "regionID", m.Region(), regionID) + default: + if m.zone.ID == 0 { + m.zone.ID = zoneID + } - return nil + return nil + } } func (m *Machine) setRingDefaults(ri *RingInfo) error { @@ -260,8 +289,13 @@ func (m *Machine) setRingDefaults(ri *RingInfo) error { // RemoveWireguardConfig deletes wgN.conf from the machine's // config directory. -func (m *Machine) RemoveWireguardConfig(ring int) error { - err := m.RemoveFile("wg%v.conf", ring) +func (m *Machine) RemoveWireguardConfig(ringID rings.RingID) error { + ring, err := AsWireguardInterfaceID(ringID) + if err != nil { + return err + } + + err = m.RemoveFile(ring.ConfFile()) if os.IsNotExist(err) { err = nil } @@ -269,7 +303,12 @@ func (m *Machine) RemoveWireguardConfig(ring int) error { return err } -func (m *Machine) createRingInfo(ring int, enabled bool) (*RingInfo, error) { +func (m *Machine) createRingInfo(ringID rings.RingID, enabled bool) (*RingInfo, error) { + ring, err := AsWireguardInterfaceID(ringID) + if err != nil { + return nil, err + } + keys, err := wireguard.NewKeyPair() if err != nil { return nil, err @@ -281,7 +320,7 @@ func (m *Machine) createRingInfo(ring int, enabled bool) (*RingInfo, error) { Keys: keys, } - err = m.applyRingInfo(ring, ri) + err = m.applyRingInfo(ringID, ri) if err != nil { return nil, err } diff --git a/pkg/cluster/machine_scan.go b/pkg/cluster/machine_scan.go index f47cabb..8d2e815 100644 --- a/pkg/cluster/machine_scan.go +++ b/pkg/cluster/machine_scan.go @@ -9,6 +9,7 @@ import ( "time" "darvaza.org/core" + "git.jpi.io/amery/jpictl/pkg/rings" ) @@ -38,8 +39,8 @@ func (m *Machine) init() error { return core.Wrap(err, m.Name) } - for i := 0; i < RingsCount; i++ { - if err := m.tryReadWireguardKeys(i); err != nil { + for _, ring := range Rings { + if err := m.tryReadWireguardKeys(ring.ID); err != nil { return core.Wrap(err, m.Name) } } @@ -72,12 +73,12 @@ func (m *Machine) setID() error { // scan is called once we know about all zones and machine names func (m *Machine) scan(_ *ScanOptions) error { - for i := 0; i < RingsCount; i++ { - if err := m.tryApplyWireguardConfig(i); err != nil { + for _, ring := range Rings { + if err := m.tryApplyWireguardConfig(ring.ID); err != nil { m.error(err). WithField("subsystem", "wireguard"). WithField("node", m.Name). - WithField("ring", i). + WithField("ring", MustWireguardInterfaceID(ring.ID)). Print() return err } diff --git a/pkg/cluster/rings.go b/pkg/cluster/rings.go index 37a123d..f29c081 100644 --- a/pkg/cluster/rings.go +++ b/pkg/cluster/rings.go @@ -4,28 +4,86 @@ import ( "fmt" "io/fs" "net/netip" + "strconv" "git.jpi.io/amery/jpictl/pkg/rings" "git.jpi.io/amery/jpictl/pkg/wireguard" ) const ( - // 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 ) +// WireguardInterfaceID represents the number in the `wg%v` +// interface name. +type WireguardInterfaceID uint + +// AsWireguardInterfaceID returns the [WireguardInterfaceID] for +// a valid [rings.RingID]. +func AsWireguardInterfaceID(ring rings.RingID) (WireguardInterfaceID, error) { + switch ring { + case rings.RingZeroID: + return 0, nil + case rings.RingOneID: + return 1, nil + default: + return 0, ErrInvalidRing(ring) + } +} + +// MustWireguardInterfaceID returns the [WireguardInterfaceID] for +// a valid [rings.RingID], and panics if it's not. +func MustWireguardInterfaceID(ring rings.RingID) WireguardInterfaceID { + id, err := AsWireguardInterfaceID(ring) + if err != nil { + panic(err) + } + return id +} + +// RingID tells the [rings.RingID] of the [WireguardInterfaceID]. +func (wi WireguardInterfaceID) RingID() rings.RingID { + return rings.RingID(wi + 1) +} + +// PubFile returns "wgN.pub" +func (wi WireguardInterfaceID) PubFile() string { + return fmt.Sprintf("wg%v.pub", wi) +} + +// KeyFile returns "wgN.key" +func (wi WireguardInterfaceID) KeyFile() string { + return fmt.Sprintf("wg%v.key", wi) +} + +// ConfFile returns "wgN.conf" +func (wi WireguardInterfaceID) ConfFile() string { + return fmt.Sprintf("wg%v.conf", wi) +} + +// Files returns all wgN.ext file names. +func (wi WireguardInterfaceID) Files() (keyFile, pubFile, confFile string) { + prefix := "wg" + strconv.Itoa(int(wi)) + + return prefix + ".key", prefix + ".pub", prefix + ".conf" +} + // RingInfo contains represents the Wireguard endpoint details // for a Machine on a particular ring type RingInfo struct { - Ring int + Ring WireguardInterfaceID Enabled bool Keys wireguard.KeyPair } +// RingID returns the [rings.RingID] for this [RingInfo]. +func (ri *RingInfo) RingID() rings.RingID { + return rings.RingID(ri.Ring + 1) +} + // Merge attempts to combine two RingInfo structs func (ri *RingInfo) Merge(alter *RingInfo) error { switch { @@ -51,7 +109,7 @@ func (ri *RingInfo) unsafeMerge(alter *RingInfo) error { ri.Enabled = true } - // fill the gaps on our keypair + // fill the gaps on our key pair if ri.Keys.PrivateKey.IsZero() { ri.Keys.PrivateKey = alter.Keys.PrivateKey } @@ -76,100 +134,34 @@ func canMergeKeyPairs(p1, p2 wireguard.KeyPair) bool { // RingAddressEncoder provides encoder/decoder access for a particular // Wireguard ring type RingAddressEncoder struct { - ID int + ID rings.RingID Port uint16 - Encode func(zoneID rings.ZoneID, nodeID rings.NodeID) (netip.Addr, bool) - Decode func(addr netip.Addr) (zoneID rings.ZoneID, nodeID rings.NodeID, ok bool) + Encode func(rings.RegionID, rings.ZoneID, rings.NodeID) (netip.Addr, error) + Decode func(addr netip.Addr) (rings.RegionID, rings.ZoneID, rings.NodeID, bool) } var ( // RingZero is a wg0 address encoder/decoder RingZero = RingAddressEncoder{ - ID: 0, + ID: rings.RingZeroID, Port: RingZeroPort, - Decode: ParseRingZeroAddress, - Encode: RingZeroAddress, + Decode: rings.DecodeRingZeroAddress, + Encode: rings.RingZeroAddress, } // RingOne is a wg1 address encoder/decoder RingOne = RingAddressEncoder{ - ID: 1, + ID: rings.RingOneID, Port: RingOnePort, - Decode: ParseRingOneAddress, - Encode: RingOneAddress, + Decode: rings.DecodeRingOneAddress, + Encode: rings.RingOneAddress, } // Rings provides indexed access to the ring address encoders - Rings = [RingsCount]RingAddressEncoder{ + Rings = []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 rings.ZoneID) bool { - return zoneID == 0 || zoneID.Valid() -} - -// 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 rings.NodeID) bool { - return nodeID == 0 || nodeID.Valid() -} - -// 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 rings.ZoneID, nodeID rings.NodeID, ok bool) { - if addr.IsValid() { - a4 := addr.As4() - - if a4[0] == 10 && a4[1] == 0 { - zoneID = rings.ZoneID(a4[2]) - nodeID = rings.NodeID(a4[3]) - return zoneID, nodeID, true - } - } - return 0, 0, false -} - -// RingZeroAddress returns a wg0 IP address -func RingZeroAddress(zoneID rings.ZoneID, nodeID rings.NodeID) (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 rings.ZoneID, nodeID rings.NodeID, ok bool) { - if addr.IsValid() { - a4 := addr.As4() - - if a4[0] == 10 && a4[2] == 0 { - zoneID = rings.ZoneID(a4[1] >> 4) - nodeID = rings.NodeID(a4[3]) - return zoneID, nodeID, true - } - } - return 0, 0, false -} - -// RingOneAddress returns a wg1 IP address -func RingOneAddress(zoneID rings.ZoneID, nodeID rings.NodeID) (netip.Addr, bool) { - switch { - case !ValidZoneID(zoneID) || !ValidNodeID(nodeID): - return netip.Addr{}, false - default: - a4 := [4]uint8{10, uint8(zoneID << 4), 0, uint8(nodeID)} - return netip.AddrFrom4(a4), true - } -} - var ( _ MachineIterator = (*Ring)(nil) _ ZoneIterator = (*Ring)(nil) @@ -192,14 +184,15 @@ func (r *Ring) AddPeer(p *Machine) bool { nodeID := p.ID zoneID := p.Zone() - addr, _ := r.Encode(zoneID, nodeID) + regionID := p.Region() + addr, _ := r.Encode(regionID, zoneID, nodeID) rp := &RingPeer{ Node: p, Address: addr, PrivateKey: ri.Keys.PrivateKey, PeerConfig: wireguard.PeerConfig{ - Name: fmt.Sprintf("%s-%v", p.Name, r.ID), + Name: fmt.Sprintf("%s-%v", p.Name, ri.Ring), PublicKey: ri.Keys.PublicKey, Endpoint: wireguard.EndpointAddress{ Host: p.FullName(), @@ -222,27 +215,27 @@ func (r *Ring) AddPeer(p *Machine) bool { } func (r *Ring) setRingZeroAllowedIPs(rp *RingPeer) { - zoneID, _, _ := r.Decode(rp.Address) + regionID, zoneID, _, _ := r.Decode(rp.Address) // everyone on ring0 is a gateway to ring1 - addr, _ := RingOneAddress(zoneID, 0) - rp.AllowCIDR(addr, 12) + subnet, _ := rings.RingOnePrefix(regionID, zoneID) + rp.AllowSubnet(subnet) // peer rp.AllowCIDR(rp.Address, 32) } func (r *Ring) setRingOneGatewayAllowedIPs(rp *RingPeer) { - zoneID, _, _ := r.Decode(rp.Address) + regionID, zoneID, _, _ := r.Decode(rp.Address) // peer rp.AllowCIDR(rp.Address, 32) // ring1 gateways connect to all other ring1 networks r.ForEachZone(func(z *Zone) bool { - if z.ID != zoneID { - addr, _ := r.Encode(z.ID, 0) - rp.AllowCIDR(addr, 12) + if !z.Is(regionID, zoneID) { + subnet := z.RingOnePrefix() + rp.AllowSubnet(subnet) } return false }) @@ -251,7 +244,7 @@ func (r *Ring) setRingOneGatewayAllowedIPs(rp *RingPeer) { r.ForEachZone(func(z *Zone) bool { z.ForEachMachine(func(p *Machine) bool { if p.IsGateway() { - addr, _ := RingZeroAddress(z.ID, p.ID) + addr, _ := p.RingZeroAddress() rp.AllowCIDR(addr, 32) } return false @@ -318,15 +311,29 @@ type RingPeer struct { // AllowCIDR allows an IP range via this peer func (rp *RingPeer) AllowCIDR(addr netip.Addr, bits int) { - cidr := netip.PrefixFrom(addr, bits) - rp.PeerConfig.AllowedIPs = append(rp.PeerConfig.AllowedIPs, cidr) + rp.AllowSubnet(netip.PrefixFrom(addr, bits)) +} + +// AllowSubnet allows an IP range via this peer +func (rp *RingPeer) AllowSubnet(subnet netip.Prefix) { + rp.PeerConfig.AllowedIPs = append(rp.PeerConfig.AllowedIPs, subnet) } // NewRing composes a new Ring for Wireguard setup -func NewRing(z ZoneIterator, m MachineIterator, ring int) (*Ring, error) { - r := &Ring{ - RingAddressEncoder: Rings[ring], - ZoneIterator: z, +func NewRing(z ZoneIterator, m MachineIterator, ringID rings.RingID) (*Ring, error) { + var r *Ring + for _, ring := range Rings { + if ringID == ring.ID { + r = &Ring{ + RingAddressEncoder: ring, + ZoneIterator: z, + } + break + } + } + + if r == nil { + return nil, ErrInvalidRing(ringID) } m.ForEachMachine(func(p *Machine) bool { diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index f3beec0..12781d8 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -35,13 +35,13 @@ func (m *Cluster) SyncMkdirAll() error { func (m *Cluster) SyncAllWireguard() error { var err error - for ring := 0; ring < RingsCount; ring++ { - err = m.WriteWireguardKeys(ring) + for _, ring := range Rings { + err = m.WriteWireguardKeys(ring.ID) if err != nil { return err } - err = m.SyncWireguardConfig(ring) + err = m.SyncWireguardConfig(ring.ID) if err != nil { return err } diff --git a/pkg/cluster/wireguard.go b/pkg/cluster/wireguard.go index b6a801a..ce71b88 100644 --- a/pkg/cluster/wireguard.go +++ b/pkg/cluster/wireguard.go @@ -3,6 +3,8 @@ package cluster import ( "io/fs" "os" + + "git.jpi.io/amery/jpictl/pkg/rings" ) var ( @@ -26,22 +28,22 @@ var ( // A WireguardConfigPruner deletes wgN.conf on all machines under // its scope with the specified ring disabled type WireguardConfigPruner interface { - PruneWireguardConfig(ring int) error + PruneWireguardConfig(ring rings.RingID) error } // PruneWireguardConfig removes wgN.conf files of machines with // the corresponding ring disabled on all zones -func (m *Cluster) PruneWireguardConfig(ring int) error { +func (m *Cluster) PruneWireguardConfig(ring rings.RingID) error { return pruneWireguardConfig(m, ring) } // PruneWireguardConfig removes wgN.conf files of machines with // the corresponding ring disabled. -func (z *Zone) PruneWireguardConfig(ring int) error { +func (z *Zone) PruneWireguardConfig(ring rings.RingID) error { return pruneWireguardConfig(z, ring) } -func pruneWireguardConfig(m MachineIterator, ring int) error { +func pruneWireguardConfig(m MachineIterator, ring rings.RingID) error { var err error m.ForEachMachine(func(p *Machine) bool { @@ -59,7 +61,7 @@ func pruneWireguardConfig(m MachineIterator, ring int) error { // PruneWireguardConfig deletes the wgN.conf file if its // presence on the ring is disabled -func (m *Machine) PruneWireguardConfig(ring int) error { +func (m *Machine) PruneWireguardConfig(ring rings.RingID) error { _, ok := m.getRingInfo(ring) if !ok { return m.RemoveWireguardConfig(ring) @@ -71,16 +73,16 @@ func (m *Machine) PruneWireguardConfig(ring int) error { // A WireguardConfigWriter rewrites all wgN.conf on all machines under // its scope attached to that ring type WireguardConfigWriter interface { - WriteWireguardConfig(ring int) error + WriteWireguardConfig(ring rings.RingID) error } // WriteWireguardConfig rewrites all wgN.conf on all machines // attached to that ring -func (m *Cluster) WriteWireguardConfig(ring int) error { +func (m *Cluster) WriteWireguardConfig(ring rings.RingID) error { switch ring { - case 0: + case rings.RingZeroID: return writeWireguardConfig(m, m, ring) - case 1: + case rings.RingOneID: var err error m.ForEachZone(func(z *Zone) bool { err = writeWireguardConfig(m, z, ring) @@ -88,24 +90,24 @@ func (m *Cluster) WriteWireguardConfig(ring int) error { }) return err default: - return fs.ErrInvalid + return ErrInvalidRing(ring) } } // WriteWireguardConfig rewrites all wgN.conf on all machines // on the Zone attached to that ring -func (z *Zone) WriteWireguardConfig(ring int) error { +func (z *Zone) WriteWireguardConfig(ring rings.RingID) error { switch ring { - case 0: + case rings.RingZeroID: return writeWireguardConfig(z.zones, z.zones, ring) - case 1: + case rings.RingOneID: return writeWireguardConfig(z.zones, z, ring) default: - return fs.ErrInvalid + return ErrInvalidRing(ring) } } -func writeWireguardConfig(z ZoneIterator, m MachineIterator, ring int) error { +func writeWireguardConfig(z ZoneIterator, m MachineIterator, ring rings.RingID) error { r, err := NewRing(z, m, ring) if err != nil { return err @@ -121,7 +123,7 @@ func writeWireguardConfig(z ZoneIterator, m MachineIterator, ring int) error { // WriteWireguardConfig rewrites the wgN.conf file of this Machine // if enabled -func (m *Machine) WriteWireguardConfig(ring int) error { +func (m *Machine) WriteWireguardConfig(ring rings.RingID) error { r, err := NewRing(m.zone.zones, m.zone, ring) if err != nil { return err @@ -131,12 +133,17 @@ func (m *Machine) WriteWireguardConfig(ring int) error { } func (m *Machine) writeWireguardRingConfig(r *Ring) error { + ring, err := AsWireguardInterfaceID(r.ID) + if err != nil { + return err + } + wg, err := r.ExportConfig(m) if err != nil { return nil } - f, err := m.CreateTruncFile("wg%v.conf", r.ID) + f, err := m.CreateTruncFile(ring.ConfFile()) if err != nil { return err } @@ -149,16 +156,16 @@ func (m *Machine) writeWireguardRingConfig(r *Ring) error { // A WireguardConfigSyncer updates all wgN.conf on all machines under // its scope reflecting the state of the ring type WireguardConfigSyncer interface { - SyncWireguardConfig(ring int) error + SyncWireguardConfig(ring rings.RingID) error } // SyncWireguardConfig updates all wgN.conf files for the specified // ring -func (m *Cluster) SyncWireguardConfig(ring int) error { +func (m *Cluster) SyncWireguardConfig(ring rings.RingID) error { switch ring { - case 0: + case rings.RingZeroID: return syncWireguardConfig(m, m, ring) - case 1: + case rings.RingOneID: var err error m.ForEachZone(func(z *Zone) bool { err = syncWireguardConfig(m, z, ring) @@ -166,24 +173,24 @@ func (m *Cluster) SyncWireguardConfig(ring int) error { }) return err default: - return fs.ErrInvalid + return ErrInvalidRing(ring) } } // SyncWireguardConfig updates all wgN.conf files for the specified // ring -func (z *Zone) SyncWireguardConfig(ring int) error { +func (z *Zone) SyncWireguardConfig(ring rings.RingID) error { switch ring { - case 0: + case rings.RingZeroID: return syncWireguardConfig(z.zones, z.zones, ring) - case 1: + case rings.RingOneID: return syncWireguardConfig(z.zones, z, ring) default: - return fs.ErrInvalid + return ErrInvalidRing(ring) } } -func syncWireguardConfig(z ZoneIterator, m MachineIterator, ring int) error { +func syncWireguardConfig(z ZoneIterator, m MachineIterator, ring rings.RingID) error { r, err := NewRing(z, m, ring) if err != nil { return err @@ -203,27 +210,27 @@ func syncWireguardConfig(z ZoneIterator, m MachineIterator, ring int) error { // SyncWireguardConfig updates all wgN.conf files for the specified // ring -func (m *Machine) SyncWireguardConfig(ring int) error { +func (m *Machine) SyncWireguardConfig(ring rings.RingID) error { return m.zone.SyncWireguardConfig(ring) } // A WireguardKeysWriter writes the Wireguard Keys for all machines // under its scope for the specified ring type WireguardKeysWriter interface { - WriteWireguardKeys(ring int) error + WriteWireguardKeys(ring rings.RingID) error } // WriteWireguardKeys rewrites all wgN.{key,pub} files -func (m *Cluster) WriteWireguardKeys(ring int) error { +func (m *Cluster) WriteWireguardKeys(ring rings.RingID) error { return writeWireguardKeys(m, ring) } // WriteWireguardKeys rewrites all wgN.{key,pub} files on this zone -func (z *Zone) WriteWireguardKeys(ring int) error { +func (z *Zone) WriteWireguardKeys(ring rings.RingID) error { return writeWireguardKeys(z, ring) } -func writeWireguardKeys(m MachineIterator, ring int) error { +func writeWireguardKeys(m MachineIterator, ring rings.RingID) error { var err error m.ForEachMachine(func(p *Machine) bool { @@ -240,12 +247,12 @@ func writeWireguardKeys(m MachineIterator, ring int) error { } // WriteWireguardKeys writes the wgN.key/wgN.pub files -func (m *Machine) WriteWireguardKeys(ring int) error { +func (m *Machine) WriteWireguardKeys(ringID rings.RingID) error { var err error var key, pub string var ri *RingInfo - ri, _ = m.getRingInfo(ring) + ri, _ = m.getRingInfo(ringID) if ri != nil { key = ri.Keys.PrivateKey.String() pub = ri.Keys.PublicKey.String() @@ -258,12 +265,13 @@ func (m *Machine) WriteWireguardKeys(ring int) error { pub = ri.Keys.PrivateKey.Public().String() } - err = m.WriteStringFile(key+"\n", "wg%v.key", ring) + keyFile, pubFile, _ := ri.Ring.Files() + err = m.WriteStringFile(key+"\n", keyFile) if err != nil { return err } - err = m.WriteStringFile(pub+"\n", "wg%v.pub", ring) + err = m.WriteStringFile(pub+"\n", pubFile) if err != nil { return err } diff --git a/pkg/cluster/zones.go b/pkg/cluster/zones.go index 47d7c91..acfc6c7 100644 --- a/pkg/cluster/zones.go +++ b/pkg/cluster/zones.go @@ -70,3 +70,38 @@ func (z *Zone) GatewayIDs() ([]rings.NodeID, int) { return out, len(out) } + +// RegionID returns the primary [Region] of a [Zone]. +func (z *Zone) RegionID() rings.RegionID { + if z != nil && z.region != nil { + return z.region.ID + } + return 0 +} + +// Is checks if the given [rings.RegionID] and [rings.ZoneID] match +// the [Zone]. +func (z *Zone) Is(regionID rings.RegionID, zoneID rings.ZoneID) bool { + switch { + case z.ID != zoneID: + return false + case z.RegionID() != regionID: + return false + default: + return true + } +} + +// Eq checks if two [Zone]s are the same. +func (z *Zone) Eq(z2 *Zone) bool { + switch { + case z == nil, z2 == nil: + return false + case z.ID != z2.ID: + return false + case z.RegionID() != z2.RegionID(): + return false + default: + return true + } +}