feat: implement KubeSpan identity controller

Fixes #4138

When KubeSpan is enabled, Talos automatically generates or loads
KubeSpan identity which consists of Wireguard key pair. ULA address is
calculated based on ClusterID and first NIC MAC address.

Some code was borrowed from #3577.

Example:

```
$ talosctl -n 172.20.0.2 get ksi
NODE         NAMESPACE   TYPE               ID      VERSION   ADDRESS                                       PUBLICKEY
172.20.0.2   kubespan    KubeSpanIdentity   local   1         fd71:6e1d:86be:6302:e871:1bff:feb2:ccee/128   Oak2fBEWngBhwslBxDVgnRNHXs88OAp4kjroSX0uqUE=
```

Additional changes:

* `--with-kubespan` flag for `talosctl cluster create` for quick testing
* validate that cluster discovery (and KubeSpan) requires ClusterID and
ClusterSecret.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
Signed-off-by: Seán C McCord <ulexus@gmail.com>
Co-authored-by: Seán C McCord <ulexus@gmail.com>
This commit is contained in:
Andrey Smirnov 2021-08-27 18:43:18 +03:00
parent da0f6e7e1d
commit caee24bf61
No known key found for this signature in database
GPG Key ID: 7B26396447AB6DFD
30 changed files with 1134 additions and 57 deletions

View File

@ -104,6 +104,7 @@ var (
encryptStatePartition bool
encryptEphemeralPartition bool
useVIP bool
enableKubeSpan bool
configPatch string
configPatchControlPlane string
configPatchWorker string
@ -382,6 +383,15 @@ func create(ctx context.Context) (err error) {
)
}
if enableKubeSpan {
genOptions = append(genOptions,
generate.WithNetworkOptions(
v1alpha1.WithKubeSpan(),
),
generate.WithClusterDiscovery(),
)
}
defaultInternalLB, defaultEndpoint := provisioner.GetLoadBalancers(request.Network)
if defaultInternalLB == "" {
@ -818,6 +828,7 @@ func init() {
createCmd.Flags().BoolVar(&encryptEphemeralPartition, "encrypt-ephemeral", false, "enable ephemeral partition encryption")
createCmd.Flags().StringVar(&talosVersion, "talos-version", "", "the desired Talos version to generate config for (if not set, defaults to image version)")
createCmd.Flags().BoolVar(&useVIP, "use-vip", false, "use a virtual IP for the controlplane endpoint instead of the loadbalancer")
createCmd.Flags().BoolVar(&enableKubeSpan, "with-kubespan", false, "enable KubeSpan system")
createCmd.Flags().StringVar(&configPatch, "config-patch", "", "patch generated machineconfigs (applied to all node types)")
createCmd.Flags().StringVar(&configPatchControlPlane, "config-patch-control-plane", "", "patch generated machineconfigs (applied to 'init' and 'controlplane' types)")
createCmd.Flags().StringVar(&configPatchWorker, "config-patch-worker", "", "patch generated machineconfigs (applied to 'worker' type)")

1
go.mod
View File

@ -58,6 +58,7 @@ require (
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43
github.com/mdlayher/genetlink v1.0.0
github.com/mdlayher/netlink v1.4.1
github.com/mdlayher/netx v0.0.0-20200512211805-669a06fde734
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417
github.com/packethost/packngo v0.19.0

2
go.sum
View File

@ -851,6 +851,8 @@ github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuri
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/mdlayher/netlink v1.4.1 h1:I154BCU+mKlIf7BgcAJB2r7QjveNPty6uNY1g9ChVfI=
github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q=
github.com/mdlayher/netx v0.0.0-20200512211805-669a06fde734 h1:DzkgdcT/W7794xU5P7GdZvok/lJECZ8g4xS+vMNLREI=
github.com/mdlayher/netx v0.0.0-20200512211805-669a06fde734/go.mod h1:iN5Y6R8oOaC0KMzLtw/dqCJ2ZOipmk+bncXKStCHr7Q=
github.com/mdlayher/raw v0.0.0-20190313224157-43dbcdd7739d/go.mod h1:r1fbeITl2xL/zLbVnNHFyOzQJTgr/3fpf1lJX/cjzR8=
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=

View File

@ -7,17 +7,15 @@ package cluster
import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"github.com/AlekSi/pointer"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/resources/cluster"
@ -60,53 +58,6 @@ func (ctrl *NodeIdentityController) Outputs() []controller.Output {
}
}
func loadOrNewFromState(statePath, path string, empty interface{}, generate func(interface{}) error) error {
fullPath := filepath.Join(statePath, path)
f, err := os.OpenFile(fullPath, os.O_RDONLY, 0)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error reading state file: %w", err)
}
// file doesn't exist yet, generate new value and save it
if f == nil {
if err = generate(empty); err != nil {
return err
}
f, err = os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o600)
if err != nil {
return fmt.Errorf("error creating state file: %w", err)
}
defer f.Close() //nolint:errcheck
encoder := yaml.NewEncoder(f)
if err = encoder.Encode(empty); err != nil {
return fmt.Errorf("error marshaling: %w", err)
}
if err = encoder.Close(); err != nil {
return err
}
return f.Close()
}
// read existing cached value
defer f.Close() //nolint:errcheck
if err = yaml.NewDecoder(f).Decode(empty); err != nil {
return fmt.Errorf("error unmarshaling: %w", err)
}
if reflect.ValueOf(empty).Elem().IsZero() {
return fmt.Errorf("value is still zero after unmarshaling")
}
return f.Close()
}
// Run implements controller.Controller interface.
//
//nolint:gocyclo
@ -136,7 +87,7 @@ func (ctrl *NodeIdentityController) Run(ctx context.Context, r controller.Runtim
var localIdentity cluster.IdentitySpec
if err := loadOrNewFromState(ctrl.StatePath, constants.NodeIdentityFilename, &localIdentity, func(v interface{}) error {
if err := controllers.LoadOrNewFromFile(filepath.Join(ctrl.StatePath, constants.NodeIdentityFilename), &localIdentity, func(v interface{}) error {
return v.(*cluster.IdentitySpec).Generate()
}); err != nil {
return fmt.Errorf("error caching node identity: %w", err)

View File

@ -0,0 +1,103 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan
import (
"context"
"fmt"
"github.com/AlekSi/pointer"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"go.uber.org/zap"
"github.com/talos-systems/talos/pkg/resources/config"
"github.com/talos-systems/talos/pkg/resources/kubespan"
)
// ConfigController watches v1alpha1.Config, updates KubeSpan config.
type ConfigController struct{}
// Name implements controller.Controller interface.
func (ctrl *ConfigController) Name() string {
return "kubespan.ConfigController"
}
// Inputs implements controller.Controller interface.
func (ctrl *ConfigController) Inputs() []controller.Input {
return []controller.Input{
{
Namespace: config.NamespaceName,
Type: config.MachineConfigType,
ID: pointer.ToString(config.V1Alpha1ID),
Kind: controller.InputWeak,
},
}
}
// Outputs implements controller.Controller interface.
func (ctrl *ConfigController) Outputs() []controller.Output {
return []controller.Output{
{
Type: kubespan.ConfigType,
Kind: controller.OutputExclusive,
},
}
}
// Run implements controller.Controller interface.
//
//nolint:gocyclo
func (ctrl *ConfigController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
for {
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
cfg, err := r.Get(ctx, resource.NewMetadata(config.NamespaceName, config.MachineConfigType, config.V1Alpha1ID, resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
return fmt.Errorf("error getting config: %w", err)
}
}
touchedIDs := make(map[resource.ID]struct{})
if cfg != nil {
c := cfg.(*config.MachineConfig).Config()
if err = r.Modify(ctx, kubespan.NewConfig(config.NamespaceName, kubespan.ConfigID), func(res resource.Resource) error {
res.(*kubespan.Config).TypedSpec().Enabled = c.Machine().Network().KubeSpan().Enabled()
res.(*kubespan.Config).TypedSpec().ClusterID = c.Cluster().ID()
return nil
}); err != nil {
return err
}
touchedIDs[kubespan.ConfigID] = struct{}{}
}
// list keys for cleanup
list, err := r.List(ctx, resource.NewMetadata(config.NamespaceName, kubespan.ConfigType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing resources: %w", err)
}
for _, res := range list.Items {
if res.Metadata().Owner() != ctrl.Name() {
continue
}
if _, ok := touchedIDs[res.Metadata().ID()]; !ok {
if err = r.Destroy(ctx, res.Metadata()); err != nil {
return fmt.Errorf("error cleaning up specs: %w", err)
}
}
}
}
}
}

View File

@ -0,0 +1,93 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan_test
import (
"testing"
"time"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/stretchr/testify/suite"
"github.com/talos-systems/go-retry/retry"
kubespanctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/kubespan"
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
"github.com/talos-systems/talos/pkg/resources/config"
"github.com/talos-systems/talos/pkg/resources/kubespan"
)
type ConfigSuite struct {
KubeSpanSuite
}
func (suite *ConfigSuite) TestReconcileConfig() {
suite.Require().NoError(suite.runtime.RegisterController(&kubespanctrl.ConfigController{}))
suite.startRuntime()
cfg := config.NewMachineConfig(&v1alpha1.Config{
ConfigVersion: "v1alpha1",
MachineConfig: &v1alpha1.MachineConfig{
MachineNetwork: &v1alpha1.NetworkConfig{
NetworkKubeSpan: v1alpha1.NetworkKubeSpan{
KubeSpanEnabled: true,
},
},
},
ClusterConfig: &v1alpha1.ClusterConfig{
ClusterID: "8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo=",
},
})
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
specMD := resource.NewMetadata(config.NamespaceName, kubespan.ConfigType, kubespan.ConfigID, resource.VersionUndefined)
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
suite.assertResource(
specMD,
func(res resource.Resource) error {
spec := res.(*kubespan.Config).TypedSpec()
suite.Assert().True(spec.Enabled)
suite.Assert().Equal("8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo=", spec.ClusterID)
return nil
},
),
))
}
func (suite *ConfigSuite) TestReconcileDisabled() {
suite.Require().NoError(suite.runtime.RegisterController(&kubespanctrl.ConfigController{}))
suite.startRuntime()
cfg := config.NewMachineConfig(&v1alpha1.Config{
ConfigVersion: "v1alpha1",
MachineConfig: &v1alpha1.MachineConfig{},
ClusterConfig: &v1alpha1.ClusterConfig{},
})
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
specMD := resource.NewMetadata(config.NamespaceName, kubespan.ConfigType, kubespan.ConfigID, resource.VersionUndefined)
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
suite.assertResource(
specMD,
func(res resource.Resource) error {
spec := res.(*kubespan.Config).TypedSpec()
suite.Assert().False(spec.Enabled)
return nil
},
),
))
}
func TestConfigSuite(t *testing.T) {
suite.Run(t, new(ConfigSuite))
}

View File

@ -0,0 +1,152 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan
import (
"context"
"fmt"
"net"
"path/filepath"
"github.com/AlekSi/pointer"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"go.uber.org/zap"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/resources/config"
"github.com/talos-systems/talos/pkg/resources/kubespan"
"github.com/talos-systems/talos/pkg/resources/network"
runtimeres "github.com/talos-systems/talos/pkg/resources/runtime"
"github.com/talos-systems/talos/pkg/resources/v1alpha1"
)
// IdentityController watches KubeSpan configuration, updates KubeSpan Identity.
type IdentityController struct {
StatePath string
}
// Name implements controller.Controller interface.
func (ctrl *IdentityController) Name() string {
return "kubespan.IdentityController"
}
// Inputs implements controller.Controller interface.
func (ctrl *IdentityController) Inputs() []controller.Input {
return []controller.Input{
{
Namespace: config.NamespaceName,
Type: kubespan.ConfigType,
ID: pointer.ToString(kubespan.ConfigID),
Kind: controller.InputWeak,
},
{
Namespace: network.NamespaceName,
Type: network.HardwareAddrType,
ID: pointer.ToString(network.FirstHardwareAddr),
Kind: controller.InputWeak,
},
{
Namespace: v1alpha1.NamespaceName,
Type: runtimeres.MountStatusType,
ID: pointer.ToString(constants.StatePartitionLabel),
Kind: controller.InputWeak,
},
}
}
// Outputs implements controller.Controller interface.
func (ctrl *IdentityController) Outputs() []controller.Output {
return []controller.Output{
{
Type: kubespan.IdentityType,
Kind: controller.OutputExclusive,
},
}
}
// Run implements controller.Controller interface.
//
//nolint:gocyclo,cyclop
func (ctrl *IdentityController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
if ctrl.StatePath == "" {
ctrl.StatePath = constants.StateMountPoint
}
for {
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
if _, err := r.Get(ctx, resource.NewMetadata(v1alpha1.NamespaceName, runtimeres.MountStatusType, constants.StatePartitionLabel, resource.VersionUndefined)); err != nil {
if state.IsNotFoundError(err) {
// wait for STATE to be mounted
continue
}
return fmt.Errorf("error reading mount status: %w", err)
}
cfg, err := r.Get(ctx, resource.NewMetadata(config.NamespaceName, kubespan.ConfigType, kubespan.ConfigID, resource.VersionUndefined))
if err != nil && !state.IsNotFoundError(err) {
return fmt.Errorf("error getting kubespan configuration: %w", err)
}
firstMAC, err := r.Get(ctx, resource.NewMetadata(network.NamespaceName, network.HardwareAddrType, network.FirstHardwareAddr, resource.VersionUndefined))
if err != nil && !state.IsNotFoundError(err) {
return fmt.Errorf("error getting first MAC address: %w", err)
}
touchedIDs := make(map[resource.ID]struct{})
if cfg != nil && firstMAC != nil && cfg.(*kubespan.Config).TypedSpec().Enabled {
var localIdentity kubespan.IdentitySpec
if err = controllers.LoadOrNewFromFile(filepath.Join(ctrl.StatePath, constants.KubeSpanIdentityFilename), &localIdentity, func(v interface{}) error {
return v.(*kubespan.IdentitySpec).GenerateKey()
}); err != nil {
return fmt.Errorf("error caching kubespan identity: %w", err)
}
kubespanCfg := cfg.(*kubespan.Config).TypedSpec()
mac := firstMAC.(*network.HardwareAddr).TypedSpec()
if err = localIdentity.UpdateAddress(kubespanCfg.ClusterID, net.HardwareAddr(mac.HardwareAddr)); err != nil {
return fmt.Errorf("error updating KubeSpan address: %w", err)
}
if err = r.Modify(ctx, kubespan.NewIdentity(kubespan.NamespaceName, kubespan.LocalIdentity), func(res resource.Resource) error {
*res.(*kubespan.Identity).TypedSpec() = localIdentity
return nil
}); err != nil {
return err
}
touchedIDs[kubespan.LocalIdentity] = struct{}{}
}
// list keys for cleanup
list, err := r.List(ctx, resource.NewMetadata(kubespan.NamespaceName, kubespan.IdentityType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing resources: %w", err)
}
for _, res := range list.Items {
if res.Metadata().Owner() != ctrl.Name() {
continue
}
if _, ok := touchedIDs[res.Metadata().ID()]; !ok {
if err = r.Destroy(ctx, res.Metadata()); err != nil {
return fmt.Errorf("error cleaning up specs: %w", err)
}
}
}
}
}
}

View File

@ -0,0 +1,141 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan_test
import (
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/stretchr/testify/suite"
"github.com/talos-systems/go-retry/retry"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
kubespanctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/kubespan"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/machinery/nethelpers"
"github.com/talos-systems/talos/pkg/resources/config"
"github.com/talos-systems/talos/pkg/resources/kubespan"
"github.com/talos-systems/talos/pkg/resources/network"
runtimeres "github.com/talos-systems/talos/pkg/resources/runtime"
"github.com/talos-systems/talos/pkg/resources/v1alpha1"
)
type IdentitySuite struct {
KubeSpanSuite
statePath string
}
func (suite *IdentitySuite) TestGenerate() {
suite.statePath = suite.T().TempDir()
suite.Require().NoError(suite.runtime.RegisterController(&kubespanctrl.IdentityController{
StatePath: suite.statePath,
}))
suite.startRuntime()
stateMount := runtimeres.NewMountStatus(v1alpha1.NamespaceName, constants.StatePartitionLabel)
suite.Assert().NoError(suite.state.Create(suite.ctx, stateMount))
cfg := kubespan.NewConfig(config.NamespaceName, kubespan.ConfigID)
cfg.TypedSpec().Enabled = true
cfg.TypedSpec().ClusterID = "8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo="
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
firstMac := network.NewHardwareAddr(network.NamespaceName, network.FirstHardwareAddr)
mac, err := net.ParseMAC("ea:71:1b:b2:cc:ee")
suite.Require().NoError(err)
firstMac.TypedSpec().HardwareAddr = nethelpers.HardwareAddr(mac)
suite.Require().NoError(suite.state.Create(suite.ctx, firstMac))
specMD := resource.NewMetadata(kubespan.NamespaceName, kubespan.IdentityType, kubespan.LocalIdentity, resource.VersionUndefined)
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
suite.assertResource(
specMD,
func(res resource.Resource) error {
spec := res.(*kubespan.Identity).TypedSpec()
_, err := wgtypes.ParseKey(spec.PrivateKey)
suite.Assert().NoError(err)
_, err = wgtypes.ParseKey(spec.PublicKey)
suite.Assert().NoError(err)
suite.Assert().Equal("fd7f:175a:b97c:5602:e871:1bff:feb2:ccee/128", spec.Address.String())
suite.Assert().Equal("fd7f:175a:b97c:5602::/64", spec.Subnet.String())
return nil
},
),
))
}
func (suite *IdentitySuite) TestLoad() {
// using verbatim data here to make sure nodeId representation is supported in future version fo Talos
const identityYaml = `address: ""
subnet: ""
privateKey: sF45u5ePau58WeeCUY3T8D9foEKaQ8Opx4cGC8g4XE4=
publicKey: Oak2fBEWngBhwslBxDVgnRNHXs88OAp4kjroSX0uqUE=
`
suite.statePath = suite.T().TempDir()
suite.Require().NoError(suite.runtime.RegisterController(&kubespanctrl.IdentityController{
StatePath: suite.statePath,
}))
suite.startRuntime()
suite.Require().NoError(os.WriteFile(filepath.Join(suite.statePath, constants.KubeSpanIdentityFilename), []byte(identityYaml), 0o600))
stateMount := runtimeres.NewMountStatus(v1alpha1.NamespaceName, constants.StatePartitionLabel)
suite.Assert().NoError(suite.state.Create(suite.ctx, stateMount))
cfg := kubespan.NewConfig(config.NamespaceName, kubespan.ConfigID)
cfg.TypedSpec().Enabled = true
cfg.TypedSpec().ClusterID = "8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo="
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
firstMac := network.NewHardwareAddr(network.NamespaceName, network.FirstHardwareAddr)
mac, err := net.ParseMAC("ea:71:1b:b2:cc:ee")
suite.Require().NoError(err)
firstMac.TypedSpec().HardwareAddr = nethelpers.HardwareAddr(mac)
suite.Require().NoError(suite.state.Create(suite.ctx, firstMac))
specMD := resource.NewMetadata(kubespan.NamespaceName, kubespan.IdentityType, kubespan.LocalIdentity, resource.VersionUndefined)
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
suite.assertResource(
specMD,
func(res resource.Resource) error {
spec := res.(*kubespan.Identity).TypedSpec()
suite.Assert().Equal("sF45u5ePau58WeeCUY3T8D9foEKaQ8Opx4cGC8g4XE4=", spec.PrivateKey)
suite.Assert().Equal("Oak2fBEWngBhwslBxDVgnRNHXs88OAp4kjroSX0uqUE=", spec.PublicKey)
suite.Assert().Equal("fd7f:175a:b97c:5602:e871:1bff:feb2:ccee/128", spec.Address.String())
suite.Assert().Equal("fd7f:175a:b97c:5602::/64", spec.Subnet.String())
return nil
},
),
))
}
func TestIdentitySuite(t *testing.T) {
suite.Run(t, new(IdentitySuite))
}

View File

@ -0,0 +1,6 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package kubespan provides controllers which manage Talos KubeSpan feature.
package kubespan

View File

@ -0,0 +1,93 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan_test
import (
"context"
"log"
"sync"
"time"
"github.com/cosi-project/runtime/pkg/controller/runtime"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/stretchr/testify/suite"
"github.com/talos-systems/go-retry/retry"
"github.com/talos-systems/talos/pkg/logging"
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
"github.com/talos-systems/talos/pkg/resources/config"
)
type KubeSpanSuite struct {
suite.Suite
state state.State
runtime *runtime.Runtime
wg sync.WaitGroup
ctx context.Context
ctxCancel context.CancelFunc
}
func (suite *KubeSpanSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute)
suite.state = state.WrapCore(namespaced.NewState(inmem.Build))
var err error
logger := logging.Wrap(log.Writer())
suite.runtime, err = runtime.NewRuntime(suite.state, logger)
suite.Require().NoError(err)
}
func (suite *KubeSpanSuite) startRuntime() {
suite.wg.Add(1)
go func() {
defer suite.wg.Done()
suite.Assert().NoError(suite.runtime.Run(suite.ctx))
}()
}
func (suite *KubeSpanSuite) assertResource(md resource.Metadata, check func(res resource.Resource) error) func() error {
return func() error {
r, err := suite.state.Get(suite.ctx, md)
if err != nil {
if state.IsNotFoundError(err) {
return retry.ExpectedError(err)
}
return err
}
return check(r)
}
}
func (suite *KubeSpanSuite) TearDownTest() {
suite.T().Log("tear down")
suite.ctxCancel()
suite.wg.Wait()
// trigger updates in resources to stop watch loops
err := suite.state.Create(context.Background(), config.NewMachineConfig(&v1alpha1.Config{
ConfigVersion: "v1alpha1",
MachineConfig: &v1alpha1.MachineConfig{},
}))
if state.IsConflictError(err) {
err = suite.state.Destroy(context.Background(), config.NewMachineConfig(nil).Metadata())
}
suite.Assert().NoError(err)
}

View File

@ -0,0 +1,60 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package controllers provides common methods for controller operations.
package controllers
import (
"fmt"
"os"
"reflect"
yaml "gopkg.in/yaml.v3"
)
// LoadOrNewFromFile either loads value from file.yaml or generates new values and saves as file.yaml.
func LoadOrNewFromFile(path string, empty interface{}, generate func(interface{}) error) error {
f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error reading state file: %w", err)
}
// file doesn't exist yet, generate new value and save it
if f == nil {
if err = generate(empty); err != nil {
return err
}
f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o600)
if err != nil {
return fmt.Errorf("error creating state file: %w", err)
}
defer f.Close() //nolint:errcheck
encoder := yaml.NewEncoder(f)
if err = encoder.Encode(empty); err != nil {
return fmt.Errorf("error marshaling: %w", err)
}
if err = encoder.Close(); err != nil {
return err
}
return f.Close()
}
// read existing cached value
defer f.Close() //nolint:errcheck
if err = yaml.NewDecoder(f).Decode(empty); err != nil {
return fmt.Errorf("error unmarshaling: %w", err)
}
if reflect.ValueOf(empty).Elem().IsZero() {
return fmt.Errorf("value is still zero after unmarshaling")
}
return f.Close()
}

View File

@ -19,6 +19,7 @@ import (
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/config"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/files"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/k8s"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/kubespan"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/perf"
runtimecontrollers "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/runtime"
@ -75,7 +76,9 @@ func (ctrl *Controller) Run(ctx context.Context) error {
&time.SyncController{
V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
},
&cluster.NodeIdentityController{},
&cluster.NodeIdentityController{
V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
},
&config.MachineTypeController{},
&config.K8sControlPlaneController{},
&files.EtcFileController{
@ -90,6 +93,8 @@ func (ctrl *Controller) Run(ctx context.Context) error {
&k8s.ManifestApplyController{},
&k8s.NodenameController{},
&k8s.RenderSecretsStaticPodController{},
&kubespan.ConfigController{},
&kubespan.IdentityController{},
&network.AddressConfigController{
Cmdline: procfs.ProcCmdline(),
V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(),

View File

@ -18,6 +18,7 @@ import (
"github.com/talos-systems/talos/pkg/resources/config"
"github.com/talos-systems/talos/pkg/resources/files"
"github.com/talos-systems/talos/pkg/resources/k8s"
"github.com/talos-systems/talos/pkg/resources/kubespan"
"github.com/talos-systems/talos/pkg/resources/network"
"github.com/talos-systems/talos/pkg/resources/perf"
"github.com/talos-systems/talos/pkg/resources/runtime"
@ -62,6 +63,7 @@ func NewState() (*State, error) {
{config.NamespaceName, "Talos node configuration."},
{files.NamespaceName, "Files and file-like resources."},
{k8s.ControlPlaneNamespaceName, "Kubernetes control plane resources."},
{kubespan.NamespaceName, "KubeSpan resources."},
{network.NamespaceName, "Networking resources."},
{network.ConfigNamespaceName, "Networking configuration resources."},
{secrets.NamespaceName, "Resources with secret material."},
@ -88,6 +90,8 @@ func NewState() (*State, error) {
&k8s.StaticPod{},
&k8s.StaticPodStatus{},
&k8s.SecretsStatus{},
&kubespan.Config{},
&kubespan.Identity{},
&network.AddressStatus{},
&network.AddressSpec{},
&network.HardwareAddr{},

View File

@ -85,6 +85,7 @@ type Input struct {
Debug bool
Persist bool
AllowSchedulingOnMasters bool
DiscoveryEnabled bool
}
// GetAPIServerEndpoint returns the formatted host:port of the API server endpoint.
@ -497,6 +498,7 @@ func NewInput(clustername, endpoint, kubernetesVersion string, secrets *SecretsB
AllowSchedulingOnMasters: options.AllowSchedulingOnMasters,
MachineDisks: options.MachineDisks,
SystemDiskEncryptionConfig: options.SystemDiskEncryptionConfig,
DiscoveryEnabled: options.DiscoveryEnabled,
}
return input, nil

View File

@ -101,6 +101,9 @@ func initUd(in *Input) (*v1alpha1.Config, error) {
ClusterAESCBCEncryptionSecret: in.Secrets.AESCBCEncryptionSecret,
ExtraManifests: []string{},
ClusterInlineManifests: v1alpha1.ClusterInlineManifests{},
ClusterDiscoveryConfig: v1alpha1.ClusterDiscoveryConfig{
DiscoveryEnabled: in.DiscoveryEnabled,
},
}
config.MachineConfig = machine

View File

@ -203,6 +203,15 @@ func WithRoles(roles role.Set) GenOption {
}
}
// WithClusterDiscovery enables cluster discovery feature.
func WithClusterDiscovery() GenOption {
return func(o *GenOptions) error {
o.DiscoveryEnabled = true
return nil
}
}
// GenOptions describes generate parameters.
type GenOptions struct {
EndpointList []string
@ -222,6 +231,7 @@ type GenOptions struct {
VersionContract *config.VersionContract
SystemDiskEncryptionConfig *v1alpha1.SystemDiskEncryptionConfig
Roles role.Set
DiscoveryEnabled bool
}
// DefaultGenOptions returns default options.

View File

@ -77,6 +77,9 @@ func workerUd(in *Input) (*v1alpha1.Config, error) {
ServiceSubnet: in.ServiceNet,
CNI: in.CNIConfig,
},
ClusterDiscoveryConfig: v1alpha1.ClusterDiscoveryConfig{
DiscoveryEnabled: in.DiscoveryEnabled,
},
}
config.MachineConfig = machine

View File

@ -120,3 +120,12 @@ func WithNetworkInterfaceVirtualIP(iface, cidr string) NetworkConfigOption {
return nil
}
}
// WithKubeSpan configures a KubeSpan interface.
func WithKubeSpan() NetworkConfigOption {
return func(_ machine.Type, cfg *NetworkConfig) error {
cfg.NetworkKubeSpan.KubeSpanEnabled = true
return nil
}
}

View File

@ -176,8 +176,18 @@ func (c *Config) Validate(mode config.RuntimeMode, options ...config.ValidationO
}
}
if c.Machine().Network().KubeSpan().Enabled() && !c.Cluster().Discovery().Enabled() {
result = multierror.Append(result, fmt.Errorf(".cluster.discovery should be enabled when .machine.network.kubespan is enabled"))
if c.Machine().Network().KubeSpan().Enabled() {
if !c.Cluster().Discovery().Enabled() {
result = multierror.Append(result, fmt.Errorf(".cluster.discovery should be enabled when .machine.network.kubespan is enabled"))
}
if c.Cluster().ID() == "" {
result = multierror.Append(result, fmt.Errorf(".cluster.id should be set when .machine.network.kubespan is enabled"))
}
if c.Cluster().Secret() == "" {
result = multierror.Append(result, fmt.Errorf(".cluster.secret should be set when .machine.network.kubespan is enabled"))
}
}
if opts.Strict {
@ -215,7 +225,7 @@ func (c *ClusterConfig) Validate() error {
result = multierror.Append(result, ecp.Validate())
}
result = multierror.Append(result, c.ClusterInlineManifests.Validate(), c.ClusterDiscoveryConfig.Validate())
result = multierror.Append(result, c.ClusterInlineManifests.Validate(), c.ClusterDiscoveryConfig.Validate(c))
return result.ErrorOrNil()
}
@ -296,14 +306,26 @@ func (manifests ClusterInlineManifests) Validate() error {
}
// Validate the discovery config.
func (c ClusterDiscoveryConfig) Validate() error {
func (c ClusterDiscoveryConfig) Validate(clusterCfg *ClusterConfig) error {
var result *multierror.Error
if !c.Enabled() {
return nil
}
if c.Registries().Service().Enabled() {
_, err := url.ParseRequestURI(c.Registries().Service().Endpoint())
if err != nil {
result = multierror.Append(result, fmt.Errorf("cluster discovery service registry endpoint is invalid: %w", err))
}
if clusterCfg.ID() == "" {
result = multierror.Append(result, fmt.Errorf("cluster discovery service requires .cluster.id"))
}
if clusterCfg.Secret() == "" {
result = multierror.Append(result, fmt.Errorf("cluster discovery service requires .cluster.secret"))
}
}
return result.ErrorOrNil()

View File

@ -744,7 +744,9 @@ func TestValidate(t *testing.T) {
},
},
},
expectedError: "1 error occurred:\n\t* .cluster.discovery should be enabled when .machine.network.kubespan is enabled\n\n",
expectedError: "3 errors occurred:\n\t* .cluster.discovery should be enabled when .machine.network.kubespan is enabled\n" +
"\t* .cluster.id should be set when .machine.network.kubespan is enabled\n" +
"\t* .cluster.secret should be set when .machine.network.kubespan is enabled\n\n",
},
{
name: "DiscoveryServiceEndpoint",
@ -754,6 +756,8 @@ func TestValidate(t *testing.T) {
MachineType: "controlplane",
},
ClusterConfig: &v1alpha1.ClusterConfig{
ClusterID: "foo",
ClusterSecret: "bar",
ControlPlane: &v1alpha1.ControlPlaneConfig{
Endpoint: &v1alpha1.Endpoint{
endpointURL,
@ -771,6 +775,26 @@ func TestValidate(t *testing.T) {
},
expectedError: "1 error occurred:\n\t* cluster discovery service registry endpoint is invalid: parse \"foo\": invalid URI for request\n\n",
},
{
name: "DiscoveryServiceClusterIDSecret",
config: &v1alpha1.Config{
ConfigVersion: "v1alpha1",
MachineConfig: &v1alpha1.MachineConfig{
MachineType: "controlplane",
},
ClusterConfig: &v1alpha1.ClusterConfig{
ControlPlane: &v1alpha1.ControlPlaneConfig{
Endpoint: &v1alpha1.Endpoint{
endpointURL,
},
},
ClusterDiscoveryConfig: v1alpha1.ClusterDiscoveryConfig{
DiscoveryEnabled: true,
},
},
},
expectedError: "2 errors occurred:\n\t* cluster discovery service requires .cluster.id\n\t* cluster discovery service requires .cluster.secret\n\n",
},
} {
test := test

View File

@ -467,6 +467,9 @@ const (
// DefaultDiscoveryServiceEndpoint is the default endpoint for Talos discovery service.
DefaultDiscoveryServiceEndpoint = "https://discovery.talos.dev/"
// KubeSpanIdentityFilename is the filename to cache KubeSpan identity across reboots.
KubeSpanIdentityFilename = "kubespan-identity.yaml"
)
// See https://linux.die.net/man/3/klogctl

View File

@ -0,0 +1,84 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan
import (
"fmt"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/meta"
"github.com/talos-systems/talos/pkg/resources/config"
)
// ConfigType is type of Config resource.
const ConfigType = resource.Type("KubeSpanConfigs.kubespan.talos.dev")
// ConfigID the singleton config resource ID.
const ConfigID = resource.ID("kubespan")
// LocalConfig is the resource ID for the local node Config.
const LocalConfig = resource.ID("local")
// Config resource holds KubeSpan configuration.
type Config struct {
md resource.Metadata
spec ConfigSpec
}
// ConfigSpec describes KubeSpan configuration..
type ConfigSpec struct {
Enabled bool `yaml:"enabled"`
ClusterID string `yaml:"clusterId"`
}
// NewConfig initializes a Config resource.
func NewConfig(namespace resource.Namespace, id resource.ID) *Config {
r := &Config{
md: resource.NewMetadata(namespace, ConfigType, id, resource.VersionUndefined),
spec: ConfigSpec{},
}
r.md.BumpVersion()
return r
}
// Metadata implements resource.Resource.
func (r *Config) Metadata() *resource.Metadata {
return &r.md
}
// Spec implements resource.Resource.
func (r *Config) Spec() interface{} {
return r.spec
}
func (r *Config) String() string {
return fmt.Sprintf("kubespan.Config(%q)", r.md.ID())
}
// DeepCopy implements resource.Resource.
func (r *Config) DeepCopy() resource.Resource {
return &Config{
md: r.md,
spec: r.spec,
}
}
// ResourceDefinition implements meta.ResourceDefinitionProvider interface.
func (r *Config) ResourceDefinition() meta.ResourceDefinitionSpec {
return meta.ResourceDefinitionSpec{
Type: ConfigType,
Aliases: []resource.Type{},
DefaultNamespace: config.NamespaceName,
PrintColumns: []meta.PrintColumn{},
}
}
// TypedSpec allows to access the Spec with the proper type.
func (r *Config) TypedSpec() *ConfigSpec {
return &r.spec
}

View File

@ -0,0 +1,125 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan
import (
"fmt"
"net"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/meta"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"inet.af/netaddr"
"github.com/talos-systems/talos/pkg/resources/network"
)
// IdentityType is type of Identity resource.
const IdentityType = resource.Type("KubeSpanIdentities.kubespan.talos.dev")
// LocalIdentity is the resource ID for the local node KubeSpan identity.
const LocalIdentity = resource.ID("local")
// Identity resource holds node identity (as a member of the cluster).
type Identity struct {
md resource.Metadata
spec IdentitySpec
}
// IdentitySpec describes KubeSpan keys and address.
//
// Note: IdentitySpec is persisted on disk in the STATE partition,
// so YAML serialization should be kept backwards compatible.
type IdentitySpec struct {
// Address of the node on the Wireguard network.
Address netaddr.IPPrefix `yaml:"address"`
Subnet netaddr.IPPrefix `yaml:"subnet"`
// Public and private Wireguard keys.
PrivateKey string `yaml:"privateKey"`
PublicKey string `yaml:"publicKey"`
}
// NewIdentity initializes a Identity resource.
func NewIdentity(namespace resource.Namespace, id resource.ID) *Identity {
r := &Identity{
md: resource.NewMetadata(namespace, IdentityType, id, resource.VersionUndefined),
spec: IdentitySpec{},
}
r.md.BumpVersion()
return r
}
// Metadata implements resource.Resource.
func (r *Identity) Metadata() *resource.Metadata {
return &r.md
}
// Spec implements resource.Resource.
func (r *Identity) Spec() interface{} {
return r.spec
}
func (r *Identity) String() string {
return fmt.Sprintf("kubespan.Identity(%q)", r.md.ID())
}
// DeepCopy implements resource.Resource.
func (r *Identity) DeepCopy() resource.Resource {
return &Identity{
md: r.md,
spec: r.spec,
}
}
// ResourceDefinition implements meta.ResourceDefinitionProvider interface.
func (r *Identity) ResourceDefinition() meta.ResourceDefinitionSpec {
return meta.ResourceDefinitionSpec{
Type: IdentityType,
Aliases: []resource.Type{},
DefaultNamespace: NamespaceName,
PrintColumns: []meta.PrintColumn{
{
Name: "Address",
JSONPath: `{.address}`,
},
{
Name: "PublicKey",
JSONPath: `{.publicKey}`,
},
},
Sensitivity: meta.Sensitive,
}
}
// TypedSpec allows to access the Spec with the proper type.
func (r *Identity) TypedSpec() *IdentitySpec {
return &r.spec
}
// GenerateKey generates new Wireguard key.
func (spec *IdentitySpec) GenerateKey() error {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return err
}
spec.PrivateKey = key.String()
spec.PublicKey = key.PublicKey().String()
return nil
}
// UpdateAddress re-calculates node address based on input data.
func (spec *IdentitySpec) UpdateAddress(clusterID string, mac net.HardwareAddr) error {
spec.Subnet = network.ULAPrefix(clusterID, network.ULAKubeSpan)
var err error
spec.Address, err = wgEUI64(spec.Subnet, mac)
return err
}

View File

@ -0,0 +1,33 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan_test
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/talos-systems/talos/pkg/resources/kubespan"
)
func TestIdentityGenerateKey(t *testing.T) {
var spec kubespan.IdentitySpec
assert.NoError(t, spec.GenerateKey())
}
func TestIdentityUpdateAddress(t *testing.T) {
var spec kubespan.IdentitySpec
mac, err := net.ParseMAC("2e:1a:b6:53:81:69")
require.NoError(t, err)
assert.NoError(t, spec.UpdateAddress("8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo=", mac))
assert.Equal(t, "fd7f:175a:b97c:5602:2c1a:b6ff:fe53:8169/128", spec.Address.String())
assert.Equal(t, "fd7f:175a:b97c:5602::/64", spec.Subnet.String())
}

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan
import "github.com/cosi-project/runtime/pkg/resource"
// NamespaceName contains resources related to KubeSpan.
const NamespaceName resource.Namespace = "kubespan"

View File

@ -0,0 +1,33 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan_test
import (
"context"
"testing"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/cosi-project/runtime/pkg/state/registry"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/talos/pkg/resources/kubespan"
)
func TestRegisterResource(t *testing.T) {
ctx := context.TODO()
resources := state.WrapCore(namespaced.NewState(inmem.Build))
resourceRegistry := registry.NewResourceRegistry(resources)
for _, resource := range []resource.Resource{
&kubespan.Config{},
&kubespan.Identity{},
} {
assert.NoError(t, resourceRegistry.Register(ctx, resource))
}
}

View File

@ -0,0 +1,31 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kubespan
import (
"fmt"
"net"
"github.com/mdlayher/netx/eui64"
"inet.af/netaddr"
)
func wgEUI64(prefix netaddr.IPPrefix, mac net.HardwareAddr) (out netaddr.IPPrefix, err error) {
if prefix.IsZero() {
return out, fmt.Errorf("cannot calculate IP from zero prefix")
}
stdIP, err := eui64.ParseMAC(prefix.IPNet().IP, mac)
if err != nil {
return out, fmt.Errorf("failed to parse MAC into EUI-64 address: %w", err)
}
ip, ok := netaddr.FromStdIP(stdIP)
if !ok {
return out, fmt.Errorf("failed to parse intermediate standard IP %q: %w", stdIP.String(), err)
}
return netaddr.IPPrefixFrom(ip, ip.BitLen()), nil
}

View File

@ -0,0 +1,45 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package network
import (
"crypto/sha256"
"inet.af/netaddr"
)
// ULAPurpose is the Unique Local Addressing key for the Talos-specific purpose of the prefix.
type ULAPurpose byte
const (
// ULAUnknown indicates an unknown ULA Purpose.
ULAUnknown = 0x00
// ULABootstrap is the Unique Local Addressing space key for the Talos Self-Bootstrapping protocol.
ULABootstrap = 0x01
// ULAKubeSpan is the Unique Local Addressing space key for the Talos KubeSpan feature.
ULAKubeSpan = 0x02
)
// ULAPrefix calculates and returns a Talos-specific Unique Local Address prefix for the given purpose.
// This implements a Talos-specific implementation of RFC4193.
// The Talos implementation uses a combination of a 48-bit cluster-unique portion with an 8-bit purpose portion.
func ULAPrefix(clusterID string, purpose ULAPurpose) netaddr.IPPrefix {
var prefixData [16]byte
hash := sha256.Sum256([]byte(clusterID))
// Take the last 16 bytes of the clusterID's hash.
copy(prefixData[:], hash[sha256.Size-16:])
// Apply the ULA prefix as per RFC4193
prefixData[0] = 0xfd
// Apply the Talos-specific ULA Purpose suffix
prefixData[7] = byte(purpose)
return netaddr.IPPrefixFrom(netaddr.IPFrom16(prefixData), 64).Masked()
}

View File

@ -0,0 +1,17 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package network_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/talos/pkg/resources/network"
)
func TestULAPrefix(t *testing.T) {
assert.Equal(t, "fd7f:175a:b97c:5602::/64", network.ULAPrefix("8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo=", network.ULAKubeSpan).String())
}

View File

@ -139,6 +139,7 @@ talosctl cluster create [flags]
--with-bootloader enable bootloader to load kernel and initramfs from disk image after install (default true)
--with-debug enable debug in Talos config to send service logs to the console
--with-init-node create the cluster with an init node
--with-kubespan enable KubeSpan system
--with-uefi enable UEFI on x86_64 architecture (always enabled for arm64)
--workers int the number of workers to create (default 1)
```