diff --git a/cmd/talosctl/cmd/talos/bootstrap.go b/cmd/talosctl/cmd/talos/bootstrap.go index 4cb8c053f0..7e2c0da721 100644 --- a/cmd/talosctl/cmd/talos/bootstrap.go +++ b/cmd/talosctl/cmd/talos/bootstrap.go @@ -52,7 +52,7 @@ Talos etcd cluster can be recovered from a known snapshot with '--recover-from=' return fmt.Errorf("error opening snapshot file: %w", err) } - defer snapshot.Close() //nolint: errcheck + defer snapshot.Close() //nolint:errcheck _, err = c.EtcdRecover(ctx, snapshot) if err != nil { diff --git a/internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go b/internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go index 970eab777c..d30f0c53c6 100644 --- a/internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go +++ b/internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go @@ -1844,13 +1844,13 @@ func (s *Server) EtcdRecover(srv machine.MachineService_EtcdRecoverServer) error return fmt.Errorf("error creating etcd recovery snapshot: %w", err) } - defer snapshot.Close() //nolint: errcheck + defer snapshot.Close() //nolint:errcheck successfulUpload := false defer func() { if !successfulUpload { - os.Remove(snapshot.Name()) //nolint: errcheck + os.Remove(snapshot.Name()) //nolint:errcheck } }() diff --git a/internal/app/machined/pkg/controllers/k8s/extra_manifest.go b/internal/app/machined/pkg/controllers/k8s/extra_manifest.go index f5d6832cff..4a267a3a6b 100644 --- a/internal/app/machined/pkg/controllers/k8s/extra_manifest.go +++ b/internal/app/machined/pkg/controllers/k8s/extra_manifest.go @@ -231,7 +231,7 @@ func (ctrl *ExtraManifestController) processInline(ctx context.Context, r contro return nil } -//nolint: dupl +//nolint:dupl func (ctrl *ExtraManifestController) teardownAll(ctx context.Context, r controller.Runtime) error { manifests, err := r.List(ctx, resource.NewMetadata(k8s.ControlPlaneNamespaceName, k8s.ManifestType, "", resource.VersionUndefined)) if err != nil { diff --git a/internal/app/machined/pkg/controllers/k8s/manifest.go b/internal/app/machined/pkg/controllers/k8s/manifest.go index f25eae82e2..3f26421480 100644 --- a/internal/app/machined/pkg/controllers/k8s/manifest.go +++ b/internal/app/machined/pkg/controllers/k8s/manifest.go @@ -230,7 +230,7 @@ func (ctrl *ManifestController) render(cfg config.K8sManifestsSpec, scrt *secret return manifests, nil } -//nolint: dupl +//nolint:dupl func (ctrl *ManifestController) teardownAll(ctx context.Context, r controller.Runtime) error { manifests, err := r.List(ctx, resource.NewMetadata(k8s.ControlPlaneNamespaceName, k8s.ManifestType, "", resource.VersionUndefined)) if err != nil { diff --git a/internal/app/machined/pkg/controllers/network/address_config.go b/internal/app/machined/pkg/controllers/network/address_config.go index 40920646d5..d4ea537fdb 100644 --- a/internal/app/machined/pkg/controllers/network/address_config.go +++ b/internal/app/machined/pkg/controllers/network/address_config.go @@ -7,9 +7,6 @@ package network import ( "context" "fmt" - "net" - "sort" - "strings" "github.com/AlekSi/pointer" "github.com/cosi-project/runtime/pkg/controller" @@ -59,7 +56,7 @@ func (ctrl *AddressConfigController) Outputs() []controller.Output { // Run implements controller.Controller interface. // -//nolint: gocyclo, cyclop +//nolint:gocyclo,cyclop func (ctrl *AddressConfigController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { // apply defaults for the loopback interface once defaultTouchedIDs, err := ctrl.apply(ctx, r, ctrl.loopbackDefaults()) @@ -141,6 +138,11 @@ func (ctrl *AddressConfigController) Run(ctx context.Context, r controller.Runti } for _, res := range list.Items { + if res.Metadata().Owner() != ctrl.Name() { + // skip specs created by other controllers + continue + } + if _, ok := touchedIDs[res.Metadata().ID()]; !ok { if err = r.Destroy(ctx, res.Metadata()); err != nil { return fmt.Errorf("error cleaning up addresses: %w", err) @@ -150,6 +152,7 @@ func (ctrl *AddressConfigController) Run(ctx context.Context, r controller.Runti } } +//nolint:dupl func (ctrl *AddressConfigController) apply(ctx context.Context, r controller.Runtime, addresses []network.AddressSpecSpec) ([]resource.ID, error) { ids := make([]string, 0, len(addresses)) @@ -202,52 +205,23 @@ func (ctrl *AddressConfigController) loopbackDefaults() []network.AddressSpecSpe } } -//nolint: gocyclo func (ctrl *AddressConfigController) parseCmdline(logger *zap.Logger) (address network.AddressSpecSpec) { if ctrl.Cmdline == nil { return } - cmdline := ctrl.Cmdline.Get("ip").First() - if cmdline == nil { - return - } - - // https://www.kernel.org/doc/Documentation/filesystems/nfs/nfsroot.txt - // ip=::::::::: - fields := strings.Split(*cmdline, ":") - - // If dhcp is specified, we'll handle it as a normal discovered - // interface - if len(fields) == 1 && fields[0] == "dhcp" { - return - } - - var err error - - address.Address.IP, err = netaddr.ParseIP(fields[0]) + settings, err := ParseCmdlineNetwork(ctrl.Cmdline) if err != nil { - logger.Info("ignoring cmdline address parse failure", zap.Error(err)) + logger.Info("ignoring cmdline parse failure", zap.Error(err)) return } - if len(fields) >= 4 { - netmask, err := netaddr.ParseIP(fields[3]) - if err != nil { - logger.Info("ignoring cmdline netmask parse failure", zap.Error(err)) - - return - } - - ones, _ := net.IPMask(netmask.IPAddr().IP).Size() - - address.Address.Bits = uint8(ones) - } else { - // default is to have complete address masked - address.Address.Bits = address.Address.IP.BitLen() + if settings.Address.IsZero() { + return } + address.Address = settings.Address if address.Address.IP.Is6() { address.Family = nethelpers.FamilyInet6 } else { @@ -256,26 +230,8 @@ func (ctrl *AddressConfigController) parseCmdline(logger *zap.Logger) (address n address.Scope = nethelpers.ScopeGlobal address.Flags = nethelpers.AddressFlags(nethelpers.AddressPermanent) - address.Layer = network.ConfigCmdline - - if len(fields) >= 6 { - address.LinkName = fields[5] - } else { - ifaces, _ := net.Interfaces() //nolint: errcheck // ignoring error here as ifaces will be empty - - sort.Slice(ifaces, func(i, j int) bool { return ifaces[i].Name < ifaces[j].Name }) - - for _, iface := range ifaces { - if iface.Flags&net.FlagLoopback != 0 { - continue - } - - address.LinkName = iface.Name - - break - } - } + address.LinkName = settings.LinkName return address } diff --git a/internal/app/machined/pkg/controllers/network/address_config_test.go b/internal/app/machined/pkg/controllers/network/address_config_test.go index c59f72b186..00a400de43 100644 --- a/internal/app/machined/pkg/controllers/network/address_config_test.go +++ b/internal/app/machined/pkg/controllers/network/address_config_test.go @@ -143,7 +143,7 @@ func (suite *AddressConfigSuite) TestCmdlineNoNetmask() { suite.startRuntime() - ifaces, _ := net.Interfaces() //nolint: errcheck // ignoring error here as ifaces will be empty + ifaces, _ := net.Interfaces() //nolint:errcheck // ignoring error here as ifaces will be empty sort.Slice(ifaces, func(i, j int) bool { return ifaces[i].Name < ifaces[j].Name }) diff --git a/internal/app/machined/pkg/controllers/network/address_merge.go b/internal/app/machined/pkg/controllers/network/address_merge.go index 4873746056..c15ddbc334 100644 --- a/internal/app/machined/pkg/controllers/network/address_merge.go +++ b/internal/app/machined/pkg/controllers/network/address_merge.go @@ -2,6 +2,7 @@ // 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 provides controllers which manage network resources. package network import ( @@ -54,7 +55,7 @@ func (ctrl *AddressMergeController) Outputs() []controller.Output { // Run implements controller.Controller interface. // -//nolint: gocyclo +//nolint:gocyclo func (ctrl *AddressMergeController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { for { select { diff --git a/internal/app/machined/pkg/controllers/network/address_spec.go b/internal/app/machined/pkg/controllers/network/address_spec.go index 6d7c69313b..7c0c3a1922 100644 --- a/internal/app/machined/pkg/controllers/network/address_spec.go +++ b/internal/app/machined/pkg/controllers/network/address_spec.go @@ -46,7 +46,7 @@ func (ctrl *AddressSpecController) Outputs() []controller.Output { // Run implements controller.Controller interface. // -//nolint: gocyclo +//nolint:gocyclo,dupl func (ctrl *AddressSpecController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { watchCh := make(chan struct{}) diff --git a/internal/app/machined/pkg/controllers/network/address_spec_test.go b/internal/app/machined/pkg/controllers/network/address_spec_test.go index 05ab0f546a..c05e0602c6 100644 --- a/internal/app/machined/pkg/controllers/network/address_spec_test.go +++ b/internal/app/machined/pkg/controllers/network/address_spec_test.go @@ -77,7 +77,7 @@ func (suite *AddressSpecSuite) assertLinkAddress(linkName, address string) error conn, err := rtnetlink.Dial(nil) suite.Require().NoError(err) - defer conn.Close() //nolint: errcheck + defer conn.Close() //nolint:errcheck linkAddresses, err := conn.Address.List() suite.Require().NoError(err) @@ -110,7 +110,7 @@ func (suite *AddressSpecSuite) assertNoLinkAddress(linkName, address string) err conn, err := rtnetlink.Dial(nil) suite.Require().NoError(err) - defer conn.Close() //nolint: errcheck + defer conn.Close() //nolint:errcheck linkAddresses, err := conn.Address.List() suite.Require().NoError(err) @@ -200,7 +200,7 @@ func (suite *AddressSpecSuite) TestDummy() { iface, err := net.InterfaceByName(dummyInterface) suite.Require().NoError(err) - defer conn.Link.Delete(uint32(iface.Index)) //nolint: errcheck + defer conn.Link.Delete(uint32(iface.Index)) //nolint:errcheck suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { diff --git a/internal/app/machined/pkg/controllers/network/cmdline.go b/internal/app/machined/pkg/controllers/network/cmdline.go new file mode 100644 index 0000000000..fd32bc8c12 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/cmdline.go @@ -0,0 +1,127 @@ +// 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 ( + "fmt" + "net" + "sort" + "strings" + + "github.com/talos-systems/go-procfs/procfs" + "inet.af/netaddr" +) + +// CmdlineNetworking contains parsed cmdline networking settings. +type CmdlineNetworking struct { + DHCP bool + Address netaddr.IPPrefix + Gateway netaddr.IP + Hostname string + LinkName string + DNSAddresses []netaddr.IP + NTPAddresses []netaddr.IP +} + +// ParseCmdlineNetwork parses `ip=` kernel cmdline argument producing all the available configuration options. +// +//nolint:gocyclo,cyclop +func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) { + var ( + settings CmdlineNetworking + err error + ) + + ipSettings := cmdline.Get("ip").First() + if ipSettings == nil { + return settings, nil + } + + // https://www.kernel.org/doc/Documentation/filesystems/nfs/nfsroot.txt + // ip=::::::::: + fields := strings.Split(*ipSettings, ":") + + // If dhcp is specified, we'll handle it as a normal discovered + // interface + if len(fields) == 1 && fields[0] == "dhcp" { + settings.DHCP = true + } + + if !settings.DHCP { + for i := range fields { + if fields[i] == "" { + continue + } + + switch i { + case 0: + settings.Address.IP, err = netaddr.ParseIP(fields[0]) + if err != nil { + return settings, fmt.Errorf("cmdline address parse failure: %s", err) + } + + // default is to have complete address masked + settings.Address.Bits = settings.Address.IP.BitLen() + case 2: + settings.Gateway, err = netaddr.ParseIP(fields[2]) + if err != nil { + return settings, fmt.Errorf("cmdline gateway parse failure: %s", err) + } + case 3: + var netmask netaddr.IP + + netmask, err = netaddr.ParseIP(fields[3]) + if err != nil { + return settings, fmt.Errorf("cmdline netmask parse failure: %s", err) + } + + ones, _ := net.IPMask(netmask.IPAddr().IP).Size() + + settings.Address.Bits = uint8(ones) + case 4: + settings.Hostname = fields[4] + case 5: + settings.LinkName = fields[5] + case 7, 8: + var dnsIP netaddr.IP + + dnsIP, err = netaddr.ParseIP(fields[i]) + if err != nil { + return settings, fmt.Errorf("error parsing DNS IP: %w", err) + } + + settings.DNSAddresses = append(settings.DNSAddresses, dnsIP) + case 9: + var ntpIP netaddr.IP + + ntpIP, err = netaddr.ParseIP(fields[i]) + if err != nil { + return settings, fmt.Errorf("error parsing DNS IP: %w", err) + } + + settings.NTPAddresses = append(settings.NTPAddresses, ntpIP) + } + } + } + + // if interface name is not set, pick the first non-loopback interface + if settings.LinkName == "" { + ifaces, _ := net.Interfaces() //nolint:errcheck // ignoring error here as ifaces will be empty + + sort.Slice(ifaces, func(i, j int) bool { return ifaces[i].Name < ifaces[j].Name }) + + for _, iface := range ifaces { + if iface.Flags&net.FlagLoopback != 0 { + continue + } + + settings.LinkName = iface.Name + + break + } + } + + return settings, nil +} diff --git a/internal/app/machined/pkg/controllers/network/cmdline_test.go b/internal/app/machined/pkg/controllers/network/cmdline_test.go new file mode 100644 index 0000000000..983e8f9a87 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/cmdline_test.go @@ -0,0 +1,110 @@ +// 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 ( + "net" + "sort" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/talos-systems/go-procfs/procfs" + "inet.af/netaddr" + + "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" +) + +type CmdlineSuite struct { + suite.Suite +} + +func (suite *CmdlineSuite) TestParse() { + ifaces, _ := net.Interfaces() //nolint:errcheck // ignoring error here as ifaces will be empty + + sort.Slice(ifaces, func(i, j int) bool { return ifaces[i].Name < ifaces[j].Name }) + + defaultIfaceName := "" + + for _, iface := range ifaces { + if iface.Flags&net.FlagLoopback != 0 { + continue + } + + defaultIfaceName = iface.Name + + break + } + + for _, test := range []struct { + name string + cmdline string + + expectedSettings network.CmdlineNetworking + expectedError string + }{ + { + name: "zero", + cmdline: "", + }, + { + name: "static IP", + cmdline: "ip=172.20.0.2::172.20.0.1:255.255.255.0::eth1:::::", + + expectedSettings: network.CmdlineNetworking{ + Address: netaddr.MustParseIPPrefix("172.20.0.2/24"), + Gateway: netaddr.MustParseIP("172.20.0.1"), + LinkName: "eth1", + }, + }, + { + name: "no iface", + cmdline: "ip=172.20.0.2::172.20.0.1", + + expectedSettings: network.CmdlineNetworking{ + Address: netaddr.MustParseIPPrefix("172.20.0.2/32"), + Gateway: netaddr.MustParseIP("172.20.0.1"), + LinkName: defaultIfaceName, + }, + }, + { + name: "complete", + cmdline: "ip=172.20.0.2:172.21.0.1:172.20.0.1:255.255.255.0:master1:eth1::10.0.0.1:10.0.0.2:10.0.0.1", + + expectedSettings: network.CmdlineNetworking{ + Address: netaddr.MustParseIPPrefix("172.20.0.2/24"), + Gateway: netaddr.MustParseIP("172.20.0.1"), + Hostname: "master1", + LinkName: "eth1", + DNSAddresses: []netaddr.IP{netaddr.MustParseIP("10.0.0.1"), netaddr.MustParseIP("10.0.0.2")}, + NTPAddresses: []netaddr.IP{netaddr.MustParseIP("10.0.0.1")}, + }, + }, + { + name: "unparseable IP", + cmdline: "ip=xyz:", + + expectedError: "cmdline address parse failure: ParseIP(\"xyz\"): unable to parse IP", + }, + } { + test := test + + suite.Run(test.name, func() { + cmdline := procfs.NewCmdline(test.cmdline) + + settings, err := network.ParseCmdlineNetwork(cmdline) + + if test.expectedError != "" { + suite.Assert().EqualError(err, test.expectedError) + } else { + suite.Assert().NoError(err) + suite.Assert().Equal(test.expectedSettings, settings) + } + }) + } +} + +func TestCmdlineSuite(t *testing.T) { + suite.Run(t, new(CmdlineSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/link_status_test.go b/internal/app/machined/pkg/controllers/network/link_status_test.go index 0cdb39fcf5..769a025d5c 100644 --- a/internal/app/machined/pkg/controllers/network/link_status_test.go +++ b/internal/app/machined/pkg/controllers/network/link_status_test.go @@ -148,7 +148,7 @@ func (suite *LinkStatusSuite) TestDummyInterface() { iface, err := net.InterfaceByName(dummyInterface) suite.Require().NoError(err) - defer conn.Link.Delete(uint32(iface.Index)) //nolint: errcheck + defer conn.Link.Delete(uint32(iface.Index)) //nolint:errcheck suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { diff --git a/internal/app/machined/pkg/controllers/network/network.go b/internal/app/machined/pkg/controllers/network/network.go index 2001a5b273..75e6582952 100644 --- a/internal/app/machined/pkg/controllers/network/network.go +++ b/internal/app/machined/pkg/controllers/network/network.go @@ -4,3 +4,6 @@ // Package network provides controllers which manage network resources. package network + +// DefaultRouteMetric is the default route metric if no metric was specified explicitly. +const DefaultRouteMetric = 1024 diff --git a/internal/app/machined/pkg/controllers/network/route_config.go b/internal/app/machined/pkg/controllers/network/route_config.go new file mode 100644 index 0000000000..da95ec5994 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/route_config.go @@ -0,0 +1,298 @@ +// 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 ( + "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" + "github.com/talos-systems/go-procfs/procfs" + "go.uber.org/zap" + "inet.af/netaddr" + + talosconfig "github.com/talos-systems/talos/pkg/machinery/config" + "github.com/talos-systems/talos/pkg/resources/config" + "github.com/talos-systems/talos/pkg/resources/network" + "github.com/talos-systems/talos/pkg/resources/network/nethelpers" +) + +// RouteConfigController manages network.RouteSpec based on machine configuration, kernel cmdline. +type RouteConfigController struct { + Cmdline *procfs.Cmdline +} + +// Name implements controller.Controller interface. +func (ctrl *RouteConfigController) Name() string { + return "network.RouteConfigController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *RouteConfigController) 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 *RouteConfigController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.RouteSpecType, + Kind: controller.OutputShared, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo,cyclop +func (ctrl *RouteConfigController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + touchedIDs := make(map[resource.ID]struct{}) + + var cfgProvider talosconfig.Provider + + 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) + } + } else { + cfgProvider = cfg.(*config.MachineConfig).Config() + } + + ignoredInterfaces := map[string]struct{}{} + + if cfgProvider != nil { + for _, device := range cfgProvider.Machine().Network().Devices() { + if device.Ignore() { + ignoredInterfaces[device.Interface()] = struct{}{} + } + } + } + + // parse kernel cmdline for the default gateway + cmdlineRoute := ctrl.parseCmdline(logger) + if !cmdlineRoute.Gateway.IsZero() { + if _, ignored := ignoredInterfaces[cmdlineRoute.OutLinkName]; !ignored { + var ids []string + + ids, err = ctrl.apply(ctx, r, []network.RouteSpecSpec{cmdlineRoute}) + if err != nil { + return fmt.Errorf("error applying cmdline route: %w", err) + } + + for _, id := range ids { + touchedIDs[id] = struct{}{} + } + } + } + + // parse machine configuration for static routes + if cfgProvider != nil { + addresses := ctrl.parseMachineConfiguration(logger, cfgProvider) + + var ids []string + + ids, err = ctrl.apply(ctx, r, addresses) + if err != nil { + return fmt.Errorf("error applying machine configuration address: %w", err) + } + + for _, id := range ids { + touchedIDs[id] = struct{}{} + } + } + + // list routes for cleanup + list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.RouteSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing resources: %w", err) + } + + for _, res := range list.Items { + if res.Metadata().Owner() != ctrl.Name() { + // skip specs created by other controllers + continue + } + + if _, ok := touchedIDs[res.Metadata().ID()]; !ok { + if err = r.Destroy(ctx, res.Metadata()); err != nil { + return fmt.Errorf("error cleaning up routes: %w", err) + } + } + } + } +} + +//nolint:dupl +func (ctrl *RouteConfigController) apply(ctx context.Context, r controller.Runtime, routes []network.RouteSpecSpec) ([]resource.ID, error) { + ids := make([]string, 0, len(routes)) + + for _, route := range routes { + route := route + id := network.LayeredID(route.Layer, network.RouteID(route.Destination, route.Gateway)) + + if err := r.Modify( + ctx, + network.NewRouteSpec(network.ConfigNamespaceName, id), + func(r resource.Resource) error { + *r.(*network.RouteSpec).Status() = route + + return nil + }, + ); err != nil { + return ids, err + } + + ids = append(ids, id) + } + + return ids, nil +} + +func (ctrl *RouteConfigController) parseCmdline(logger *zap.Logger) (route network.RouteSpecSpec) { + if ctrl.Cmdline == nil { + return + } + + settings, err := ParseCmdlineNetwork(ctrl.Cmdline) + if err != nil { + logger.Info("ignoring error", zap.Error(err)) + + return + } + + if settings.Gateway.IsZero() { + return + } + + route.Gateway = settings.Gateway + + if route.Gateway.Is6() { + route.Family = nethelpers.FamilyInet6 + } else { + route.Family = nethelpers.FamilyInet4 + } + + route.Scope = nethelpers.ScopeGlobal + route.Table = nethelpers.TableMain + route.Priority = DefaultRouteMetric + route.Protocol = nethelpers.ProtocolBoot + route.Type = nethelpers.TypeUnicast + route.OutLinkName = settings.LinkName + route.Layer = network.ConfigCmdline + + return route +} + +var ( + zero16 = netaddr.MustParseIP("::") + zero4 = netaddr.MustParseIP("0.0.0.0") +) + +//nolint:gocyclo,cyclop +func (ctrl *RouteConfigController) parseMachineConfiguration(logger *zap.Logger, cfgProvider talosconfig.Provider) (routes []network.RouteSpecSpec) { + convert := func(linkName string, in talosconfig.Route) (route network.RouteSpecSpec, err error) { + if in.Network() != "" { + route.Destination, err = netaddr.ParseIPPrefix(in.Network()) + if err != nil { + return route, fmt.Errorf("error parsing route network: %w", err) + } + + if route.Destination.Bits == 0 && (route.Destination.IP.Compare(zero4) == 0 || route.Destination.IP.Compare(zero16) == 0) { + // clear destination to be zero value to support "0.0.0.0/0" routes + route.Destination = netaddr.IPPrefix{} + } + } + + route.Gateway, err = netaddr.ParseIP(in.Gateway()) + if err != nil { + return route, fmt.Errorf("error parsing route gateway: %w", err) + } + + route.Priority = in.Metric() + if route.Priority == 0 { + route.Priority = DefaultRouteMetric + } + + if route.Gateway.Is6() { + route.Family = nethelpers.FamilyInet6 + } else { + route.Family = nethelpers.FamilyInet4 + } + + route.Table = nethelpers.TableMain + route.Protocol = nethelpers.ProtocolStatic + route.OutLinkName = linkName + route.Layer = network.ConfigMachineConfiguration + + switch { + case route.Destination.IP.IsLinkLocalUnicast() || route.Destination.IP.IsLinkLocalMulticast(): + route.Scope = nethelpers.ScopeLink + case route.Destination.IP.IsLoopback(): + route.Scope = nethelpers.ScopeHost + default: + route.Scope = nethelpers.ScopeGlobal + } + + route.Type = nethelpers.TypeUnicast + + if route.Destination.IP.IsMulticast() { + route.Type = nethelpers.TypeMulticast + } + + return route, nil + } + + for _, device := range cfgProvider.Machine().Network().Devices() { + if device.Ignore() { + continue + } + + for _, route := range device.Routes() { + routeSpec, err := convert(device.Interface(), route) + if err != nil { + logger.Sugar().Infof("skipping route %q -> %q on interface %q: %s", route.Network(), route.Gateway(), device.Interface(), err) + + continue + } + + routes = append(routes, routeSpec) + } + + for _, vlan := range device.Vlans() { + vlanLinkName := fmt.Sprintf("%s.%d", device.Interface(), vlan.ID()) + + for _, route := range vlan.Routes() { + routeSpec, err := convert(vlanLinkName, route) + if err != nil { + logger.Sugar().Infof("skipping route %q -> %q on interface %q: %s", route.Network(), route.Gateway(), vlanLinkName, err) + + continue + } + + routes = append(routes, routeSpec) + } + } + } + + return routes +} diff --git a/internal/app/machined/pkg/controllers/network/route_config_test.go b/internal/app/machined/pkg/controllers/network/route_config_test.go new file mode 100644 index 0000000000..b85107eecb --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/route_config_test.go @@ -0,0 +1,226 @@ +// 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/. + +//nolint:dupl +package network_test + +import ( + "context" + "fmt" + "log" + "net/url" + "sync" + "testing" + "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-procfs/procfs" + "github.com/talos-systems/go-retry/retry" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "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" + "github.com/talos-systems/talos/pkg/resources/network" + "github.com/talos-systems/talos/pkg/resources/network/nethelpers" +) + +type RouteConfigSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *RouteConfigSuite) SetupTest() { + suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute) + + suite.state = state.WrapCore(namespaced.NewState(inmem.Build)) + + var err error + + suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer())) + suite.Require().NoError(err) +} + +func (suite *RouteConfigSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *RouteConfigSuite) assertRoutes(requiredIDs []string, check func(*network.RouteSpec) error) error { + missingIDs := make(map[string]struct{}, len(requiredIDs)) + + for _, id := range requiredIDs { + missingIDs[id] = struct{}{} + } + + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.RouteSpecType, "", resource.VersionUndefined)) + if err != nil { + return retry.UnexpectedError(err) + } + + for _, res := range resources.Items { + _, required := missingIDs[res.Metadata().ID()] + if !required { + continue + } + + delete(missingIDs, res.Metadata().ID()) + + if err = check(res.(*network.RouteSpec)); err != nil { + return retry.ExpectedError(err) + } + } + + if len(missingIDs) > 0 { + return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) + } + + return nil +} + +func (suite *RouteConfigSuite) TestCmdline() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.RouteConfigController{ + Cmdline: procfs.NewCmdline("ip=172.20.0.2::172.20.0.1:255.255.255.0::eth1:::::"), + })) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertRoutes([]string{ + "cmdline//172.20.0.1", + }, func(r *network.RouteSpec) error { + suite.Assert().Equal("eth1", r.Status().OutLinkName) + suite.Assert().Equal(network.ConfigCmdline, r.Status().Layer) + suite.Assert().Equal(nethelpers.FamilyInet4, r.Status().Family) + suite.Assert().EqualValues(netctrl.DefaultRouteMetric, r.Status().Priority) + + return nil + }) + })) +} + +func (suite *RouteConfigSuite) TestMachineConfiguration() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.RouteConfigController{})) + + suite.startRuntime() + + u, err := url.Parse("https://foo:6443") + suite.Require().NoError(err) + + cfg := config.NewMachineConfig(&v1alpha1.Config{ + ConfigVersion: "v1alpha1", + MachineConfig: &v1alpha1.MachineConfig{ + MachineNetwork: &v1alpha1.NetworkConfig{ + NetworkInterfaces: []*v1alpha1.Device{ + { + DeviceInterface: "eth3", + DeviceCIDR: "192.168.0.24/28", + DeviceRoutes: []*v1alpha1.Route{ + { + RouteNetwork: "192.168.0.0/18", + RouteGateway: "192.168.0.25", + RouteMetric: 25, + }, + }, + }, + { + DeviceIgnore: true, + DeviceInterface: "eth4", + DeviceCIDR: "192.168.0.24/28", + DeviceRoutes: []*v1alpha1.Route{ + { + RouteNetwork: "192.168.0.0/18", + RouteGateway: "192.168.0.26", + RouteMetric: 25, + }, + }, + }, + { + DeviceInterface: "eth2", + DeviceCIDR: "2001:470:6d:30e:8ed2:b60c:9d2f:803a/64", + DeviceRoutes: []*v1alpha1.Route{ + { + RouteGateway: "2001:470:6d:30e:8ed2:b60c:9d2f:803b", + }, + }, + }, + { + DeviceInterface: "eth0", + DeviceVlans: []*v1alpha1.Vlan{ + { + VlanID: 24, + VlanCIDR: "10.0.0.1/8", + VlanRoutes: []*v1alpha1.Route{ + { + RouteNetwork: "10.0.3.0/24", + RouteGateway: "10.0.3.1", + }, + }, + }, + }, + }, + }, + }, + }, + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{ + URL: u, + }, + }, + }, + }) + + suite.Require().NoError(suite.state.Create(suite.ctx, cfg)) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertRoutes([]string{ + "configuration//2001:470:6d:30e:8ed2:b60c:9d2f:803b", + "configuration/10.0.3.0/24/10.0.3.1", + "configuration/192.168.0.0/18/192.168.0.25", + }, func(r *network.RouteSpec) error { + switch r.Metadata().ID() { + case "configuration//2001:470:6d:30e:8ed2:b60c:9d2f:803b": + suite.Assert().Equal("eth2", r.Status().OutLinkName) + suite.Assert().Equal(nethelpers.FamilyInet6, r.Status().Family) + suite.Assert().EqualValues(netctrl.DefaultRouteMetric, r.Status().Priority) + case "configuration/10.0.3.0/24/10.0.3.1": + suite.Assert().Equal("eth0.24", r.Status().OutLinkName) + suite.Assert().Equal(nethelpers.FamilyInet4, r.Status().Family) + suite.Assert().EqualValues(netctrl.DefaultRouteMetric, r.Status().Priority) + case "configuration/192.168.0.0/18/192.168.0.25": + suite.Assert().Equal("eth3", r.Status().OutLinkName) + suite.Assert().Equal(nethelpers.FamilyInet4, r.Status().Family) + suite.Assert().EqualValues(25, r.Status().Priority) + } + + suite.Assert().Equal(network.ConfigMachineConfiguration, r.Status().Layer) + + return nil + }) + })) +} + +func TestRouteConfigSuite(t *testing.T) { + suite.Run(t, new(RouteConfigSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/route_merge.go b/internal/app/machined/pkg/controllers/network/route_merge.go new file mode 100644 index 0000000000..5fb09f248d --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/route_merge.go @@ -0,0 +1,126 @@ +// 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 provides controllers which manage network resources. +package network + +import ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "go.uber.org/zap" + + "github.com/talos-systems/talos/pkg/resources/network" +) + +// RouteMergeController merges network.RouteSpec in network.ConfigNamespace and produces final network.RouteSpec in network.Namespace. +type RouteMergeController struct{} + +// Name implements controller.Controller interface. +func (ctrl *RouteMergeController) Name() string { + return "network.RouteMergeController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *RouteMergeController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: network.ConfigNamespaceName, + Type: network.RouteSpecType, + Kind: controller.InputWeak, + }, + // TODO: temporary hack to make controller watch its outputs to facilitate proper teardown sequence + // should be fixed in the runtime library to automatically support notifications on finalizer change + // on outputs + { + Namespace: network.NamespaceName, + Type: network.RouteSpecType, + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *RouteMergeController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.RouteSpecType, + Kind: controller.OutputShared, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *RouteMergeController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + // list source network configuration resources + list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.RouteSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing source network routes: %w", err) + } + + // route is allowed as long as it's not duplicate, for duplicate higher layer takes precedence + routes := map[string]*network.RouteSpec{} + + for _, res := range list.Items { + route := res.(*network.RouteSpec) //nolint:errcheck,forcetypeassert + id := network.RouteID(route.Status().Destination, route.Status().Gateway) + + existing, ok := routes[id] + if ok && existing.Status().Layer > route.Status().Layer { + // skip this route, as existing one is higher layer + continue + } + + routes[id] = route + } + + for id, route := range routes { + route := route + + if err = r.Modify(ctx, network.NewRouteSpec(network.NamespaceName, id), func(res resource.Resource) error { + rt := res.(*network.RouteSpec) //nolint:errcheck,forcetypeassert + + *rt.Status() = *route.Status() + + return nil + }); err != nil { + return fmt.Errorf("error updating resource: %w", err) + } + } + + // list routes for cleanup + list, err = r.List(ctx, resource.NewMetadata(network.NamespaceName, network.RouteSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing resources: %w", err) + } + + for _, res := range list.Items { + if _, ok := routes[res.Metadata().ID()]; !ok { + var okToDestroy bool + + okToDestroy, err = r.Teardown(ctx, res.Metadata()) + if err != nil { + return fmt.Errorf("error cleaning up addresses: %w", err) + } + + if okToDestroy { + if err = r.Destroy(ctx, res.Metadata()); err != nil { + return fmt.Errorf("error cleaning up addresses: %w", err) + } + } + } + } + } +} diff --git a/internal/app/machined/pkg/controllers/network/route_merge_test.go b/internal/app/machined/pkg/controllers/network/route_merge_test.go new file mode 100644 index 0000000000..839cfc75b8 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/route_merge_test.go @@ -0,0 +1,203 @@ +// 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/. + +//nolint:dupl +package network_test + +import ( + "context" + "fmt" + "log" + "sync" + "testing" + "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" + "inet.af/netaddr" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/resources/network" + "github.com/talos-systems/talos/pkg/resources/network/nethelpers" +) + +type RouteMergeSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *RouteMergeSuite) SetupTest() { + suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute) + + suite.state = state.WrapCore(namespaced.NewState(inmem.Build)) + + var err error + + suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer())) + suite.Require().NoError(err) + + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.RouteMergeController{})) + + suite.startRuntime() +} + +func (suite *RouteMergeSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *RouteMergeSuite) assertRoutes(requiredIDs []string, check func(*network.RouteSpec) error) error { + missingIDs := make(map[string]struct{}, len(requiredIDs)) + + for _, id := range requiredIDs { + missingIDs[id] = struct{}{} + } + + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.RouteSpecType, "", resource.VersionUndefined)) + if err != nil { + return retry.UnexpectedError(err) + } + + for _, res := range resources.Items { + _, required := missingIDs[res.Metadata().ID()] + if !required { + continue + } + + delete(missingIDs, res.Metadata().ID()) + + if err = check(res.(*network.RouteSpec)); err != nil { + return retry.ExpectedError(err) + } + } + + if len(missingIDs) > 0 { + return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) + } + + return nil +} + +func (suite *RouteMergeSuite) assertNoRoute(id string) error { + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.AddressStatusType, "", resource.VersionUndefined)) + if err != nil { + return retry.UnexpectedError(err) + } + + for _, res := range resources.Items { + if res.Metadata().ID() == id { + return retry.ExpectedError(fmt.Errorf("address %q is still there", id)) + } + } + + return nil +} + +func (suite *RouteMergeSuite) TestMerge() { + cmdline := network.NewRouteSpec(network.ConfigNamespaceName, "cmdline//10.5.0.3") + *cmdline.Status() = network.RouteSpecSpec{ + Gateway: netaddr.MustParseIP("10.5.0.3"), + OutLinkName: "eth0", + Family: nethelpers.FamilyInet4, + Scope: nethelpers.ScopeGlobal, + Type: nethelpers.TypeUnicast, + Priority: 1024, + Layer: network.ConfigCmdline, + } + + dhcp := network.NewRouteSpec(network.ConfigNamespaceName, "dhcp//10.5.0.3") + *dhcp.Status() = network.RouteSpecSpec{ + Gateway: netaddr.MustParseIP("10.5.0.3"), + OutLinkName: "eth0", + Family: nethelpers.FamilyInet4, + Scope: nethelpers.ScopeGlobal, + Type: nethelpers.TypeUnicast, + Priority: 50, + Layer: network.ConfigDHCP, + } + + static := network.NewRouteSpec(network.ConfigNamespaceName, "configuration/10.0.0.35/32/10.0.0.34") + *static.Status() = network.RouteSpecSpec{ + Destination: netaddr.MustParseIPPrefix("10.0.0.35/32"), + Gateway: netaddr.MustParseIP("10.0.0.34"), + OutLinkName: "eth0", + Family: nethelpers.FamilyInet4, + Scope: nethelpers.ScopeGlobal, + Type: nethelpers.TypeUnicast, + Priority: 1024, + Layer: network.ConfigMachineConfiguration, + } + + for _, res := range []resource.Resource{cmdline, dhcp, static} { + suite.Require().NoError(suite.state.Create(suite.ctx, res), "%v", res.Spec()) + } + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertRoutes([]string{ + "/10.5.0.3", + "10.0.0.35/32/10.0.0.34", + }, func(r *network.RouteSpec) error { + switch r.Metadata().ID() { + case "/10.5.0.3": + suite.Assert().Equal(*dhcp.Status(), *r.Status()) + case "10.0.0.35/32/10.0.0.34": + suite.Assert().Equal(*static.Status(), *r.Status()) + } + + return nil + }) + })) + + suite.Require().NoError(suite.state.Destroy(suite.ctx, dhcp.Metadata())) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertRoutes([]string{ + "/10.5.0.3", + "10.0.0.35/32/10.0.0.34", + }, func(r *network.RouteSpec) error { + switch r.Metadata().ID() { + case "/10.5.0.3": + if *cmdline.Status() != *r.Status() { + // using retry here, as it might not be reconciled immediately + return retry.ExpectedError(fmt.Errorf("not equal yet")) + } + case "10.0.0.35/32/10.0.0.34": + suite.Assert().Equal(*static.Status(), *r.Status()) + } + + return nil + }) + })) + + suite.Require().NoError(suite.state.Destroy(suite.ctx, static.Metadata())) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertNoRoute("10.0.0.35/32/10.0.0.34") + })) +} + +func TestRouteMergeSuite(t *testing.T) { + suite.Run(t, new(RouteMergeSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/route_spec.go b/internal/app/machined/pkg/controllers/network/route_spec.go new file mode 100644 index 0000000000..eaa9849fc9 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/route_spec.go @@ -0,0 +1,219 @@ +// 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 ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/jsimonetti/rtnetlink" + "go.uber.org/zap" + "golang.org/x/sys/unix" + "inet.af/netaddr" + + "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network/watch" + "github.com/talos-systems/talos/pkg/resources/network" +) + +// RouteSpecController applies network.RouteSpec to the actual interfaces. +type RouteSpecController struct{} + +// Name implements controller.Controller interface. +func (ctrl *RouteSpecController) Name() string { + return "network.RouteSpecController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *RouteSpecController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: network.NamespaceName, + Type: network.RouteSpecType, + Kind: controller.InputStrong, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *RouteSpecController) Outputs() []controller.Output { + return nil +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo,dupl +func (ctrl *RouteSpecController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + watchCh := make(chan struct{}) + + // watch link changes as some routes might need to be re-applied if the link appears + watcher, err := watch.NewRtNetlink(ctx, watchCh, unix.RTMGRP_LINK) + if err != nil { + return err + } + + defer watcher.Done() + + conn, err := rtnetlink.Dial(nil) + if err != nil { + return fmt.Errorf("error dialing rtnetlink socket: %w", err) + } + + defer conn.Close() //nolint:errcheck + + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + case <-watchCh: + } + + // list source network configuration resources + list, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.RouteSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing source network addresses: %w", err) + } + + // add finalizers for all live resources + for _, res := range list.Items { + if res.Metadata().Phase() != resource.PhaseRunning { + continue + } + + if err = r.AddFinalizer(ctx, res.Metadata(), ctrl.Name()); err != nil { + return fmt.Errorf("error adding finalizer: %w", err) + } + } + + // list rtnetlink links (interfaces) + links, err := conn.Link.List() + if err != nil { + return fmt.Errorf("error listing links: %w", err) + } + + // list rtnetlink routes + routes, err := conn.Route.List() + if err != nil { + return fmt.Errorf("error listing addresses: %w", err) + } + + // loop over route and make reconcile decision + for _, res := range list.Items { + address := res.(*network.RouteSpec) //nolint:forcetypeassert,errcheck + + if err = ctrl.syncRoute(ctx, r, logger, conn, links, routes, address); err != nil { + return err + } + } + } +} + +func findRoutes(routes []rtnetlink.RouteMessage, destination netaddr.IPPrefix, gateway netaddr.IP) []*rtnetlink.RouteMessage { + var result []*rtnetlink.RouteMessage //nolint:prealloc + + for i, route := range routes { + if route.DstLength != destination.Bits { + continue + } + + if !route.Attributes.Dst.Equal(destination.IP.IPAddr().IP) { + continue + } + + if !route.Attributes.Gateway.Equal(gateway.IPAddr().IP) { + continue + } + + result = append(result, &routes[i]) + } + + return result +} + +//nolint:gocyclo +func (ctrl *RouteSpecController) syncRoute(ctx context.Context, r controller.Runtime, logger *zap.Logger, conn *rtnetlink.Conn, + links []rtnetlink.LinkMessage, routes []rtnetlink.RouteMessage, route *network.RouteSpec) error { + linkIndex := resolveLinkName(links, route.Status().OutLinkName) + + destinationStr := route.Status().Destination.String() + + if route.Status().Destination.IsZero() { + destinationStr = "default" + } + + switch route.Metadata().Phase() { + case resource.PhaseTearingDown: + for _, existing := range findRoutes(routes, route.Status().Destination, route.Status().Gateway) { + // delete route + if err := conn.Route.Delete(existing); err != nil { + return fmt.Errorf("error removing route: %w", err) + } + + logger.Sugar().Infof("removed route to %s via %s (link %q)", destinationStr, route.Status().Gateway, route.Status().OutLinkName) + } + + // now remove finalizer as address was deleted + if err := r.RemoveFinalizer(ctx, route.Metadata(), ctrl.Name()); err != nil { + return fmt.Errorf("error removing finalizer: %w", err) + } + case resource.PhaseRunning: + if linkIndex == 0 && route.Status().OutLinkName != "" { + // route can't be created as link doesn't exist (yet), skip it + return nil + } + + matchFound := false + + for _, existing := range findRoutes(routes, route.Status().Destination, route.Status().Gateway) { + // check if existing matches the spec: if it does, skip update + if existing.Scope == uint8(route.Status().Scope) && existing.Flags == uint32(route.Status().Flags) && + existing.Protocol == uint8(route.Status().Protocol) && existing.Flags == uint32(route.Status().Flags) && + existing.Attributes.OutIface == linkIndex && existing.Attributes.Priority == route.Status().Priority && + existing.Attributes.Table == uint32(route.Status().Table) { + matchFound = true + + break + } + + // delete route, it doesn't match the spec + if err := conn.Route.Delete(existing); err != nil { + return fmt.Errorf("error removing route: %w", err) + } + + logger.Sugar().Infof("removed route to %s via %s (link %q)", destinationStr, route.Status().Gateway, route.Status().OutLinkName) + } + + if matchFound { + return nil + } + + // add route + msg := &rtnetlink.RouteMessage{ + Family: uint8(route.Status().Family), + DstLength: route.Status().Destination.Bits, + Protocol: uint8(route.Status().Protocol), + Scope: uint8(route.Status().Scope), + Type: uint8(route.Status().Type), + Flags: uint32(route.Status().Flags), + Attributes: rtnetlink.RouteAttributes{ + Dst: route.Status().Destination.IP.IPAddr().IP, + Gateway: route.Status().Gateway.IPAddr().IP, + OutIface: linkIndex, + Priority: route.Status().Priority, + Table: uint32(route.Status().Table), + }, + } + + if err := conn.Route.Add(msg); err != nil { + return fmt.Errorf("error adding route: %w, message %+v", err, *msg) + } + + logger.Sugar().Infof("created route to %s via %s (link %q)", destinationStr, route.Status().Gateway, route.Status().OutLinkName) + } + + return nil +} diff --git a/internal/app/machined/pkg/controllers/network/route_spec_test.go b/internal/app/machined/pkg/controllers/network/route_spec_test.go new file mode 100644 index 0000000000..a9ab4f777d --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/route_spec_test.go @@ -0,0 +1,246 @@ +// 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/. + +//nolint:dupl +package network_test + +import ( + "context" + "fmt" + "log" + "sync" + "testing" + "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/jsimonetti/rtnetlink" + "github.com/stretchr/testify/suite" + "github.com/talos-systems/go-retry/retry" + "inet.af/netaddr" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/resources/network" + "github.com/talos-systems/talos/pkg/resources/network/nethelpers" +) + +type RouteSpecSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *RouteSpecSuite) SetupTest() { + suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute) + + suite.state = state.WrapCore(namespaced.NewState(inmem.Build)) + + var err error + + suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer())) + suite.Require().NoError(err) + + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.RouteSpecController{})) + + suite.startRuntime() +} + +func (suite *RouteSpecSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *RouteSpecSuite) assertRoute(destination netaddr.IPPrefix, gateway netaddr.IP, check func(rtnetlink.RouteMessage) error) error { + conn, err := rtnetlink.Dial(nil) + suite.Require().NoError(err) + + defer conn.Close() //nolint:errcheck + + routes, err := conn.Route.List() + suite.Require().NoError(err) + + matching := 0 + + for _, route := range routes { + if !gateway.IPAddr().IP.Equal(route.Attributes.Gateway) { + continue + } + + if route.DstLength != destination.Bits { + continue + } + + if !destination.IP.IPAddr().IP.Equal(route.Attributes.Dst) { + continue + } + + matching++ + + if err = check(route); err != nil { + return retry.ExpectedError(err) + } + } + + switch { + case matching == 1: + return nil + case matching == 0: + return retry.ExpectedError(fmt.Errorf("route to %s via %s not found", destination, gateway)) + default: + return retry.ExpectedError(fmt.Errorf("route to %s via %s found %d matches", destination, gateway, matching)) + } +} + +func (suite *RouteSpecSuite) assertNoRoute(destination netaddr.IPPrefix, gateway netaddr.IP) error { + conn, err := rtnetlink.Dial(nil) + suite.Require().NoError(err) + + defer conn.Close() //nolint:errcheck + + routes, err := conn.Route.List() + suite.Require().NoError(err) + + for _, route := range routes { + if gateway.IPAddr().IP.Equal(route.Attributes.Gateway) && destination.Bits == route.DstLength && destination.IP.IPAddr().IP.Equal(route.Attributes.Dst) { + return retry.ExpectedError(fmt.Errorf("route to %s via %s is present", destination, gateway)) + } + } + + return nil +} + +func (suite *RouteSpecSuite) TestLoopback() { + loopback := network.NewRouteSpec(network.NamespaceName, "loopback") + *loopback.Status() = network.RouteSpecSpec{ + Family: nethelpers.FamilyInet4, + Destination: netaddr.MustParseIPPrefix("127.0.11.0/24"), + Gateway: netaddr.MustParseIP("127.0.11.1"), + OutLinkName: "lo", + Scope: nethelpers.ScopeGlobal, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Layer: network.ConfigMachineConfiguration, + } + + for _, res := range []resource.Resource{loopback} { + suite.Require().NoError(suite.state.Create(suite.ctx, res), "%v", res.Spec()) + } + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertRoute(netaddr.MustParseIPPrefix("127.0.11.0/24"), netaddr.MustParseIP("127.0.11.1"), func(route rtnetlink.RouteMessage) error { + suite.Assert().EqualValues(0, route.Attributes.Priority) + + return nil + }) + })) + + // teardown the route + for { + ready, err := suite.state.Teardown(suite.ctx, loopback.Metadata()) + suite.Require().NoError(err) + + if ready { + break + } + + time.Sleep(100 * time.Millisecond) + } + + // torn down address should be removed immediately + suite.Assert().NoError(suite.assertNoRoute(netaddr.MustParseIPPrefix("127.0.11.0/24"), netaddr.MustParseIP("127.0.11.1"))) + + suite.Require().NoError(suite.state.Destroy(suite.ctx, loopback.Metadata())) +} + +func (suite *RouteSpecSuite) TestDefaultRoute() { + // adding default route with high metric to avoid messing up with the actual default route + def := network.NewRouteSpec(network.NamespaceName, "default") + *def.Status() = network.RouteSpecSpec{ + Family: nethelpers.FamilyInet4, + Destination: netaddr.IPPrefix{}, + Gateway: netaddr.MustParseIP("127.0.11.2"), + Scope: nethelpers.ScopeGlobal, + Table: nethelpers.TableMain, + OutLinkName: "lo", + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Priority: 1048576, + Layer: network.ConfigMachineConfiguration, + } + + for _, res := range []resource.Resource{def} { + suite.Require().NoError(suite.state.Create(suite.ctx, res), "%v", res.Spec()) + } + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertRoute(netaddr.IPPrefix{}, netaddr.MustParseIP("127.0.11.2"), func(route rtnetlink.RouteMessage) error { + suite.Assert().Nil(route.Attributes.Dst) + suite.Assert().EqualValues(1048576, route.Attributes.Priority) + + return nil + }) + })) + + // update the route metric + _, err := suite.state.UpdateWithConflicts(suite.ctx, def.Metadata(), func(r resource.Resource) error { + defR := r.(*network.RouteSpec) //nolint:forcetypeassert,errcheck + + defR.Status().Priority = 1048577 + + return nil + }) + suite.Assert().NoError(err) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertRoute(netaddr.IPPrefix{}, netaddr.MustParseIP("127.0.11.2"), func(route rtnetlink.RouteMessage) error { + suite.Assert().Nil(route.Attributes.Dst) + + if route.Attributes.Priority != 1048577 { + return fmt.Errorf("route metric wasn't updated: %d", route.Attributes.Priority) + } + + return nil + }) + })) + + // teardown the route + for { + ready, err := suite.state.Teardown(suite.ctx, def.Metadata()) + suite.Require().NoError(err) + + if ready { + break + } + + time.Sleep(100 * time.Millisecond) + } + + // torn down route should be removed immediately + suite.Assert().NoError(suite.assertNoRoute(netaddr.IPPrefix{}, netaddr.MustParseIP("127.0.11.2"))) + + suite.Require().NoError(suite.state.Destroy(suite.ctx, def.Metadata())) +} + +func TestRouteSpecSuite(t *testing.T) { + suite.Run(t, new(RouteSpecSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/route_status.go b/internal/app/machined/pkg/controllers/network/route_status.go new file mode 100644 index 0000000000..9110d3e1a0 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/route_status.go @@ -0,0 +1,145 @@ +// 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 ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/jsimonetti/rtnetlink" + "go.uber.org/zap" + "golang.org/x/sys/unix" + "inet.af/netaddr" + + "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network/watch" + "github.com/talos-systems/talos/pkg/resources/network" + "github.com/talos-systems/talos/pkg/resources/network/nethelpers" +) + +// RouteStatusController manages secrets.Etcd based on configuration. +type RouteStatusController struct{} + +// Name implements controller.Controller interface. +func (ctrl *RouteStatusController) Name() string { + return "network.RouteStatusController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *RouteStatusController) Inputs() []controller.Input { + return nil +} + +// Outputs implements controller.Controller interface. +func (ctrl *RouteStatusController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.RouteStatusType, + Kind: controller.OutputExclusive, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *RouteStatusController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + watchCh := make(chan struct{}) + + watcher, err := watch.NewRtNetlink(ctx, watchCh, unix.RTMGRP_IPV4_MROUTE|unix.RTMGRP_IPV4_ROUTE|unix.RTMGRP_IPV6_MROUTE|unix.RTMGRP_IPV6_ROUTE) + if err != nil { + return err + } + + defer watcher.Done() + + conn, err := rtnetlink.Dial(nil) + if err != nil { + return fmt.Errorf("error dialing rtnetlink socket: %w", err) + } + + defer conn.Close() //nolint:errcheck + + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + case <-watchCh: + } + + // build links lookup table + links, err := conn.Link.List() + if err != nil { + return fmt.Errorf("error listing links: %w", err) + } + + linkLookup := make(map[uint32]string, len(links)) + + for _, link := range links { + linkLookup[link.Index] = link.Attributes.Name + } + + // list resources for cleanup + list, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.RouteStatusType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing resources: %w", err) + } + + itemsToDelete := map[resource.ID]struct{}{} + + for _, r := range list.Items { + itemsToDelete[r.Metadata().ID()] = struct{}{} + } + + routes, err := conn.Route.List() + if err != nil { + return fmt.Errorf("error listing routes: %w", err) + } + + for _, route := range routes { + route := route + + dstAddr, _ := netaddr.FromStdIPRaw(route.Attributes.Dst) + dstPrefix := netaddr.IPPrefix{ + IP: dstAddr, + Bits: route.DstLength, + } + gatewayAddr, _ := netaddr.FromStdIPRaw(route.Attributes.Gateway) + id := network.RouteID(dstPrefix, gatewayAddr) + + if err = r.Modify(ctx, network.NewRouteStatus(network.NamespaceName, id), func(r resource.Resource) error { + status := r.(*network.RouteStatus).Status() + + status.Family = nethelpers.Family(route.Family) + status.Destination = dstPrefix + status.Source.IP, _ = netaddr.FromStdIPRaw(route.Attributes.Src) + status.Source.Bits = route.SrcLength + status.Gateway = gatewayAddr + status.OutLinkIndex = route.Attributes.OutIface + status.OutLinkName = linkLookup[route.Attributes.OutIface] + status.Priority = route.Attributes.Priority + status.Table = nethelpers.RoutingTable(route.Table) + status.Scope = nethelpers.Scope(route.Scope) + status.Type = nethelpers.RouteType(route.Type) + status.Protocol = nethelpers.RouteProtocol(route.Protocol) + status.Flags = nethelpers.RouteFlags(route.Flags) + + return nil + }); err != nil { + return fmt.Errorf("error modifying resource: %w", err) + } + + delete(itemsToDelete, id) + } + + for id := range itemsToDelete { + if err = r.Destroy(ctx, resource.NewMetadata(network.NamespaceName, network.RouteStatusType, id, resource.VersionUndefined)); err != nil { + return fmt.Errorf("error deleting route status %q: %w", id, err) + } + } + } +} diff --git a/internal/app/machined/pkg/controllers/network/route_status_test.go b/internal/app/machined/pkg/controllers/network/route_status_test.go new file mode 100644 index 0000000000..361d0adb96 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/route_status_test.go @@ -0,0 +1,117 @@ +// 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/. + +//nolint:dupl +package network_test + +import ( + "context" + "fmt" + "log" + "sync" + "testing" + "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" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/resources/network" + "github.com/talos-systems/talos/pkg/resources/network/nethelpers" +) + +type RouteStatusSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *RouteStatusSuite) SetupTest() { + suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute) + + suite.state = state.WrapCore(namespaced.NewState(inmem.Build)) + + var err error + + suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer())) + suite.Require().NoError(err) + + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.RouteStatusController{})) + + suite.startRuntime() +} + +func (suite *RouteStatusSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *RouteStatusSuite) assertRoutes(requiredIDs []string, check func(*network.RouteStatus) error) error { + missingIDs := make(map[string]struct{}, len(requiredIDs)) + + for _, id := range requiredIDs { + missingIDs[id] = struct{}{} + } + + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.RouteStatusType, "", resource.VersionUndefined)) + if err != nil { + return retry.UnexpectedError(err) + } + + for _, res := range resources.Items { + _, required := missingIDs[res.Metadata().ID()] + if !required { + continue + } + + delete(missingIDs, res.Metadata().ID()) + + if err = check(res.(*network.RouteStatus)); err != nil { + return retry.ExpectedError(err) + } + } + + if len(missingIDs) > 0 { + return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) + } + + return nil +} + +func (suite *RouteStatusSuite) TestRoutes() { + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertRoutes([]string{"127.0.0.0/8/"}, func(r *network.RouteStatus) error { + suite.Assert().True(r.Status().Source.IP.IsLoopback()) + suite.Assert().Equal("lo", r.Status().OutLinkName) + suite.Assert().Equal(nethelpers.TableLocal, r.Status().Table) + suite.Assert().Equal(nethelpers.ScopeHost, r.Status().Scope) + suite.Assert().Equal(nethelpers.TypeLocal, r.Status().Type) + suite.Assert().Equal(nethelpers.ProtocolKernel, r.Status().Protocol) + + return nil + }) + })) +} + +func TestRouteStatusSuite(t *testing.T) { + suite.Run(t, new(RouteStatusSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/watch/ethtool.go b/internal/app/machined/pkg/controllers/network/watch/ethtool.go index d74e77c4f6..d75e17c7af 100644 --- a/internal/app/machined/pkg/controllers/network/watch/ethtool.go +++ b/internal/app/machined/pkg/controllers/network/watch/ethtool.go @@ -14,16 +14,19 @@ import ( ) type ethtoolWatcher struct { - wg sync.WaitGroup - conn *genetlink.Conn + wg sync.WaitGroup + conn *genetlink.Conn + cancel context.CancelFunc } // NewEthtool starts ethtool watch. // -//nolint: gocyclo +//nolint:gocyclo func NewEthtool(ctx context.Context, watchCh chan<- struct{}) (Watcher, error) { watcher := ðtoolWatcher{} + ctx, watcher.cancel = context.WithCancel(ctx) + var err error watcher.conn, err = genetlink.Dial(nil) @@ -77,7 +80,8 @@ func NewEthtool(ctx context.Context, watchCh chan<- struct{}) (Watcher, error) { } func (watcher *ethtoolWatcher) Done() { - watcher.conn.Close() //nolint: errcheck + watcher.cancel() + watcher.conn.Close() //nolint:errcheck watcher.wg.Wait() } diff --git a/internal/app/machined/pkg/controllers/network/watch/rtnetlink.go b/internal/app/machined/pkg/controllers/network/watch/rtnetlink.go index ededb7558f..5d1fcd5adc 100644 --- a/internal/app/machined/pkg/controllers/network/watch/rtnetlink.go +++ b/internal/app/machined/pkg/controllers/network/watch/rtnetlink.go @@ -14,14 +14,17 @@ import ( ) type rtnetlinkWatcher struct { - wg sync.WaitGroup - conn *rtnetlink.Conn + wg sync.WaitGroup + cancel context.CancelFunc + conn *rtnetlink.Conn } // NewRtNetlink starts rtnetlink watch over specified groups. func NewRtNetlink(ctx context.Context, watchCh chan<- struct{}, groups uint32) (Watcher, error) { watcher := &rtnetlinkWatcher{} + ctx, watcher.cancel = context.WithCancel(ctx) + var err error watcher.conn, err = rtnetlink.Dial(&netlink.Config{ @@ -54,7 +57,8 @@ func NewRtNetlink(ctx context.Context, watchCh chan<- struct{}, groups uint32) ( } func (watcher *rtnetlinkWatcher) Done() { - watcher.conn.Close() //nolint: errcheck + watcher.cancel() + watcher.conn.Close() //nolint:errcheck watcher.wg.Wait() } diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go index a597bf7a9d..0ac15d856a 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go @@ -76,6 +76,12 @@ func (ctrl *Controller) Run(ctx context.Context) error { &network.AddressSpecController{}, &network.AddressStatusController{}, &network.LinkStatusController{}, + &network.RouteConfigController{ + Cmdline: procfs.ProcCmdline(), + }, + &network.RouteMergeController{}, + &network.RouteStatusController{}, + &network.RouteSpecController{}, &secrets.EtcdController{}, &secrets.KubernetesController{}, &secrets.RootController{}, diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go index 06eee64075..053404b261 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go @@ -80,6 +80,8 @@ func NewState() (*State, error) { &network.AddressStatus{}, &network.AddressSpec{}, &network.LinkStatus{}, + &network.RouteStatus{}, + &network.RouteSpec{}, &secrets.Etcd{}, &secrets.Kubernetes{}, &secrets.Root{}, diff --git a/internal/integration/api/etcd-recover.go b/internal/integration/api/etcd-recover.go index e2bde16220..e63382fac4 100644 --- a/internal/integration/api/etcd-recover.go +++ b/internal/integration/api/etcd-recover.go @@ -56,7 +56,7 @@ func (suite *EtcdRecoverSuite) TearDownTest() { // TestSnapshotRecover snapshot etcd, wipes control plane nodes and recovers etcd from a snapshot. // -//nolint: gocyclo +//nolint:gocyclo func (suite *EtcdRecoverSuite) TestSnapshotRecover() { if !suite.Capabilities().SupportsReboot { suite.T().Skip("cluster doesn't support reboot") diff --git a/pkg/cluster/sonobuoy/sonobuoy.go b/pkg/cluster/sonobuoy/sonobuoy.go index b991766712..ff105d12ea 100644 --- a/pkg/cluster/sonobuoy/sonobuoy.go +++ b/pkg/cluster/sonobuoy/sonobuoy.go @@ -260,7 +260,7 @@ func Run(ctx context.Context, cluster cluster.K8sProvider, options *Options) err return err } - matched, _ := filepath.Match("tmp/sonobuoy/*_sonobuoy_*.tar.gz", header.Name) //nolint: errcheck + matched, _ := filepath.Match("tmp/sonobuoy/*_sonobuoy_*.tar.gz", header.Name) //nolint:errcheck if !matched { continue @@ -273,7 +273,7 @@ func Run(ctx context.Context, cluster cluster.K8sProvider, options *Options) err return err } - defer gzipR.Close() //nolint: errcheck + defer gzipR.Close() //nolint:errcheck innnerTarR := tar.NewReader(gzipR) diff --git a/pkg/resources/network/nethelpers/routeflag_string.go b/pkg/resources/network/nethelpers/routeflag_string.go new file mode 100644 index 0000000000..05f670f6c3 --- /dev/null +++ b/pkg/resources/network/nethelpers/routeflag_string.go @@ -0,0 +1,37 @@ +// Code generated by "stringer -type=RouteFlag -linecomment"; DO NOT EDIT. + +package nethelpers + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[RouteNotify-256] + _ = x[RouteCloned-512] + _ = x[RouteEqualize-1024] + _ = x[RoutePrefix-2048] +} + +const ( + _RouteFlag_name_0 = "notify" + _RouteFlag_name_1 = "cloned" + _RouteFlag_name_2 = "equalize" + _RouteFlag_name_3 = "prefix" +) + +func (i RouteFlag) String() string { + switch { + case i == 256: + return _RouteFlag_name_0 + case i == 512: + return _RouteFlag_name_1 + case i == 1024: + return _RouteFlag_name_2 + case i == 2048: + return _RouteFlag_name_3 + default: + return "RouteFlag(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/pkg/resources/network/nethelpers/routeflags.go b/pkg/resources/network/nethelpers/routeflags.go new file mode 100644 index 0000000000..028557399c --- /dev/null +++ b/pkg/resources/network/nethelpers/routeflags.go @@ -0,0 +1,44 @@ +// 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 nethelpers + +//go:generate stringer -type=RouteFlag -linecomment + +import ( + "strings" + + "golang.org/x/sys/unix" +) + +// RouteFlags is a bitmask of RouteFlag. +type RouteFlags uint32 + +func (flags RouteFlags) String() string { + var values []string + + for flag := RouteNotify; flag <= RoutePrefix; flag <<= 1 { + if (RouteFlag(flags) & flag) == flag { + values = append(values, flag.String()) + } + } + + return strings.Join(values, ",") +} + +// MarshalYAML implements yaml.Marshaler. +func (flags RouteFlags) MarshalYAML() (interface{}, error) { + return flags.String(), nil +} + +// RouteFlag wraps RTM_F_* constants. +type RouteFlag uint32 + +// RouteFlag constants. +const ( + RouteNotify RouteFlag = unix.RTM_F_NOTIFY // notify + RouteCloned RouteFlag = unix.RTM_F_CLONED // cloned + RouteEqualize RouteFlag = unix.RTM_F_EQUALIZE // equalize + RoutePrefix RouteFlag = unix.RTM_F_PREFIX // prefix +) diff --git a/pkg/resources/network/nethelpers/routeprotocol.go b/pkg/resources/network/nethelpers/routeprotocol.go new file mode 100644 index 0000000000..ad2ab20b8d --- /dev/null +++ b/pkg/resources/network/nethelpers/routeprotocol.go @@ -0,0 +1,26 @@ +// 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 nethelpers + +import "golang.org/x/sys/unix" + +//go:generate stringer -type=RouteProtocol -linecomment + +// RouteProtocol is a routing protocol. +type RouteProtocol uint8 + +// MarshalYAML implements yaml.Marshaler. +func (rp RouteProtocol) MarshalYAML() (interface{}, error) { + return rp.String(), nil +} + +// RouteType constants. +const ( + ProtocolUnspec RouteProtocol = unix.RTPROT_UNSPEC // unspec + ProtocolRedirect RouteProtocol = unix.RTPROT_REDIRECT // redirect + ProtocolKernel RouteProtocol = unix.RTPROT_KERNEL // kernel + ProtocolBoot RouteProtocol = unix.RTPROT_BOOT // boot + ProtocolStatic RouteProtocol = unix.RTPROT_STATIC // static +) diff --git a/pkg/resources/network/nethelpers/routeprotocol_string.go b/pkg/resources/network/nethelpers/routeprotocol_string.go new file mode 100644 index 0000000000..6d80b15b4f --- /dev/null +++ b/pkg/resources/network/nethelpers/routeprotocol_string.go @@ -0,0 +1,27 @@ +// Code generated by "stringer -type=RouteProtocol -linecomment"; DO NOT EDIT. + +package nethelpers + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ProtocolUnspec-0] + _ = x[ProtocolRedirect-1] + _ = x[ProtocolKernel-2] + _ = x[ProtocolBoot-3] + _ = x[ProtocolStatic-4] +} + +const _RouteProtocol_name = "unspecredirectkernelbootstatic" + +var _RouteProtocol_index = [...]uint8{0, 6, 14, 20, 24, 30} + +func (i RouteProtocol) String() string { + if i >= RouteProtocol(len(_RouteProtocol_index)-1) { + return "RouteProtocol(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _RouteProtocol_name[_RouteProtocol_index[i]:_RouteProtocol_index[i+1]] +} diff --git a/pkg/resources/network/nethelpers/routetype.go b/pkg/resources/network/nethelpers/routetype.go new file mode 100644 index 0000000000..56eaa2ea49 --- /dev/null +++ b/pkg/resources/network/nethelpers/routetype.go @@ -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 nethelpers + +import "golang.org/x/sys/unix" + +//go:generate stringer -type=RouteType -linecomment + +// RouteType is a route type. +type RouteType uint8 + +// MarshalYAML implements yaml.Marshaler. +func (rt RouteType) MarshalYAML() (interface{}, error) { + return rt.String(), nil +} + +// RouteType constants. +const ( + TypeUnspec RouteType = unix.RTN_UNSPEC // unspec + TypeUnicast RouteType = unix.RTN_UNICAST // unicast + TypeLocal RouteType = unix.RTN_LOCAL // local + TypeBroadcast RouteType = unix.RTN_BROADCAST // broadcast + TypeAnycast RouteType = unix.RTN_ANYCAST // anycast + TypeMulticast RouteType = unix.RTN_MULTICAST // multicast + TypeBlackhole RouteType = unix.RTN_BLACKHOLE // blackhole + TypeUnreachable RouteType = unix.RTN_UNREACHABLE // unreachable + TypeProhibit RouteType = unix.RTN_PROHIBIT // prohibit + TypeThrow RouteType = unix.RTN_THROW // throw + TypeNAT RouteType = unix.RTN_NAT // nat + TypeXResolve RouteType = unix.RTN_XRESOLVE // xresolve +) diff --git a/pkg/resources/network/nethelpers/routetype_string.go b/pkg/resources/network/nethelpers/routetype_string.go new file mode 100644 index 0000000000..c5e370fdc0 --- /dev/null +++ b/pkg/resources/network/nethelpers/routetype_string.go @@ -0,0 +1,34 @@ +// Code generated by "stringer -type=RouteType -linecomment"; DO NOT EDIT. + +package nethelpers + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TypeUnspec-0] + _ = x[TypeUnicast-1] + _ = x[TypeLocal-2] + _ = x[TypeBroadcast-3] + _ = x[TypeAnycast-4] + _ = x[TypeMulticast-5] + _ = x[TypeBlackhole-6] + _ = x[TypeUnreachable-7] + _ = x[TypeProhibit-8] + _ = x[TypeThrow-9] + _ = x[TypeNAT-10] + _ = x[TypeXResolve-11] +} + +const _RouteType_name = "unspecunicastlocalbroadcastanycastmulticastblackholeunreachableprohibitthrownatxresolve" + +var _RouteType_index = [...]uint8{0, 6, 13, 18, 27, 34, 43, 52, 63, 71, 76, 79, 87} + +func (i RouteType) String() string { + if i >= RouteType(len(_RouteType_index)-1) { + return "RouteType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _RouteType_name[_RouteType_index[i]:_RouteType_index[i+1]] +} diff --git a/pkg/resources/network/nethelpers/routingtable.go b/pkg/resources/network/nethelpers/routingtable.go new file mode 100644 index 0000000000..46f0ab36fb --- /dev/null +++ b/pkg/resources/network/nethelpers/routingtable.go @@ -0,0 +1,25 @@ +// 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 nethelpers + +import "golang.org/x/sys/unix" + +//go:generate stringer -type=RoutingTable -linecomment + +// RoutingTable is a routing table ID. +type RoutingTable uint32 + +// MarshalYAML implements yaml.Marshaler. +func (table RoutingTable) MarshalYAML() (interface{}, error) { + return table.String(), nil +} + +// RoutingTable constants. +const ( + TableUnspec RoutingTable = unix.RT_TABLE_UNSPEC // unspec + TableDefault RoutingTable = unix.RT_TABLE_DEFAULT // default + TableMain RoutingTable = unix.RT_TABLE_MAIN // main + TableLocal RoutingTable = unix.RT_TABLE_LOCAL // local +) diff --git a/pkg/resources/network/nethelpers/routingtable_string.go b/pkg/resources/network/nethelpers/routingtable_string.go new file mode 100644 index 0000000000..1ec3a15af8 --- /dev/null +++ b/pkg/resources/network/nethelpers/routingtable_string.go @@ -0,0 +1,36 @@ +// Code generated by "stringer -type=RoutingTable -linecomment"; DO NOT EDIT. + +package nethelpers + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TableUnspec-0] + _ = x[TableDefault-253] + _ = x[TableMain-254] + _ = x[TableLocal-255] +} + +const ( + _RoutingTable_name_0 = "unspec" + _RoutingTable_name_1 = "defaultmainlocal" +) + +var ( + _RoutingTable_index_1 = [...]uint8{0, 7, 11, 16} +) + +func (i RoutingTable) String() string { + switch { + case i == 0: + return _RoutingTable_name_0 + case 253 <= i && i <= 255: + i -= 253 + return _RoutingTable_name_1[_RoutingTable_index_1[i]:_RoutingTable_index_1[i+1]] + default: + return "RoutingTable(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/pkg/resources/network/network.go b/pkg/resources/network/network.go index b3efc15155..c8174e576a 100644 --- a/pkg/resources/network/network.go +++ b/pkg/resources/network/network.go @@ -25,6 +25,14 @@ func AddressID(linkName string, addr netaddr.IPPrefix) string { return fmt.Sprintf("%s/%s", linkName, addr) } +// RouteID builds ID (primary key) for the route. +func RouteID(destination netaddr.IPPrefix, gateway netaddr.IP) string { + dst, _ := destination.MarshalText() //nolint:errcheck + gw, _ := gateway.MarshalText() //nolint:errcheck + + return fmt.Sprintf("%s/%s", string(dst), string(gw)) +} + // LayeredID builds configuration for the entity at some layer. func LayeredID(layer ConfigLayer, id string) string { return fmt.Sprintf("%s/%s", layer, id) diff --git a/pkg/resources/network/network_test.go b/pkg/resources/network/network_test.go index 02b8e965b5..b645acc074 100644 --- a/pkg/resources/network/network_test.go +++ b/pkg/resources/network/network_test.go @@ -28,6 +28,8 @@ func TestRegisterResource(t *testing.T) { &network.AddressStatus{}, &network.AddressSpec{}, &network.LinkStatus{}, + &network.RouteStatus{}, + &network.RouteSpec{}, } { assert.NoError(t, resourceRegistry.Register(ctx, resource)) } diff --git a/pkg/resources/network/route_spec.go b/pkg/resources/network/route_spec.go new file mode 100644 index 0000000000..a7f3bcd0e9 --- /dev/null +++ b/pkg/resources/network/route_spec.go @@ -0,0 +1,88 @@ +// 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 ( + "fmt" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "inet.af/netaddr" + + "github.com/talos-systems/talos/pkg/resources/network/nethelpers" +) + +// RouteSpecType is type of RouteSpec resource. +const RouteSpecType = resource.Type("RouteSpecs.net.talos.dev") + +// RouteSpec resource holds route specification to be applied to the kernel. +type RouteSpec struct { + md resource.Metadata + spec RouteSpecSpec +} + +// RouteSpecSpec describes the route. +type RouteSpecSpec struct { + Family nethelpers.Family `yaml:"family"` + Destination netaddr.IPPrefix `yaml:"dst"` + Gateway netaddr.IP `yaml:"gateway"` + OutLinkName string `yaml:"outLinkName,omitempty"` + Table nethelpers.RoutingTable `yaml:"table"` + Priority uint32 `yaml:"priority,omitempty"` + Scope nethelpers.Scope `yaml:"scope"` + Type nethelpers.RouteType `yaml:"type"` + Flags nethelpers.RouteFlags `yaml:"flags"` + Protocol nethelpers.RouteProtocol `yaml:"protocol"` + Layer ConfigLayer `yaml:"layer"` +} + +// NewRouteSpec initializes a SecretsStatus resource. +func NewRouteSpec(namespace resource.Namespace, id resource.ID) *RouteSpec { + r := &RouteSpec{ + md: resource.NewMetadata(namespace, RouteSpecType, id, resource.VersionUndefined), + spec: RouteSpecSpec{}, + } + + r.md.BumpVersion() + + return r +} + +// Metadata implements resource.Resource. +func (r *RouteSpec) Metadata() *resource.Metadata { + return &r.md +} + +// Spec implements resource.Resource. +func (r *RouteSpec) Spec() interface{} { + return r.spec +} + +func (r *RouteSpec) String() string { + return fmt.Sprintf("network.RouteSpec(%q)", r.md.ID()) +} + +// DeepCopy implements resource.Resource. +func (r *RouteSpec) DeepCopy() resource.Resource { + return &RouteSpec{ + md: r.md, + spec: r.spec, + } +} + +// ResourceDefinition implements meta.ResourceDefinitionProvider interface. +func (r *RouteSpec) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: RouteSpecType, + Aliases: []resource.Type{}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{}, + } +} + +// Status sets pod status. +func (r *RouteSpec) Status() *RouteSpecSpec { + return &r.spec +} diff --git a/pkg/resources/network/route_status.go b/pkg/resources/network/route_status.go new file mode 100644 index 0000000000..b3a43d598e --- /dev/null +++ b/pkg/resources/network/route_status.go @@ -0,0 +1,106 @@ +// 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 ( + "fmt" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "inet.af/netaddr" + + "github.com/talos-systems/talos/pkg/resources/network/nethelpers" +) + +// RouteStatusType is type of RouteStatus resource. +const RouteStatusType = resource.Type("RouteStatuses.net.talos.dev") + +// RouteStatus resource holds physical network link status. +type RouteStatus struct { + md resource.Metadata + spec RouteStatusSpec +} + +// RouteStatusSpec describes status of rendered secrets. +type RouteStatusSpec struct { + Family nethelpers.Family `yaml:"family"` + Destination netaddr.IPPrefix `yaml:"dst"` + Source netaddr.IPPrefix `yaml:"src"` + Gateway netaddr.IP `yaml:"gateway"` + OutLinkIndex uint32 `yaml:"outLinkIndex,omitempty"` + OutLinkName string `yaml:"outLinkName,omitempty"` + Table nethelpers.RoutingTable `yaml:"table"` + Priority uint32 `yaml:"priority"` + Scope nethelpers.Scope `yaml:"scope"` + Type nethelpers.RouteType `yaml:"type"` + Flags nethelpers.RouteFlags `yaml:"flags"` + Protocol nethelpers.RouteProtocol `yaml:"protocol"` +} + +// NewRouteStatus initializes a SecretsStatus resource. +func NewRouteStatus(namespace resource.Namespace, id resource.ID) *RouteStatus { + r := &RouteStatus{ + md: resource.NewMetadata(namespace, RouteStatusType, id, resource.VersionUndefined), + spec: RouteStatusSpec{}, + } + + r.md.BumpVersion() + + return r +} + +// Metadata implements resource.Resource. +func (r *RouteStatus) Metadata() *resource.Metadata { + return &r.md +} + +// Spec implements resource.Resource. +func (r *RouteStatus) Spec() interface{} { + return r.spec +} + +func (r *RouteStatus) String() string { + return fmt.Sprintf("network.RouteStatus(%q)", r.md.ID()) +} + +// DeepCopy implements resource.Resource. +func (r *RouteStatus) DeepCopy() resource.Resource { + return &RouteStatus{ + md: r.md, + spec: r.spec, + } +} + +// ResourceDefinition implements meta.ResourceDefinitionProvider interface. +func (r *RouteStatus) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: RouteStatusType, + Aliases: []resource.Type{"route", "routes"}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{ + { + Name: "Destination", + JSONPath: `{.dst}`, + }, + { + Name: "Gateway", + JSONPath: `{.gateway}`, + }, + { + Name: "Link", + JSONPath: `{.outLinkName}`, + }, + { + Name: "Metric", + JSONPath: `{.priority}`, + }, + }, + } +} + +// Status sets pod status. +func (r *RouteStatus) Status() *RouteStatusSpec { + return &r.spec +}