diff --git a/internal/controller/network/doc.go b/internal/controller/network/doc.go new file mode 100644 index 0000000000..772074d408 --- /dev/null +++ b/internal/controller/network/doc.go @@ -0,0 +1,42 @@ +//go:build windows + +// Package network provides a controller for managing the network lifecycle of a pod +// running inside a Utility VM (UVM). +// +// It handles attaching an HCN namespace and its endpoints to the guest VM, +// and tearing them down on pod removal. The [Controller] interface is the +// primary entry point, with [Manager] as its concrete implementation. +// +// # Lifecycle +// +// A network follows the state machine below. +// +// ┌────────────────────┐ +// │ StateNotConfigured │ +// └───┬────────────┬───┘ +// Setup ok │ │ Setup fails +// ▼ ▼ +// ┌─────────────────┐ ┌──────────────┐ +// │ StateConfigured │ │ StateInvalid │ +// └────────┬────────┘ └──────┬───────┘ +// │ Teardown │ Teardown +// ▼ ▼ +// ┌─────────────────────────────────────┐ +// │ StateTornDown │ +// └─────────────────────────────────────┘ +// +// State descriptions: +// +// - [StateNotConfigured]: initial state; no namespace or NICs have been configured. +// - [StateConfigured]: after [Controller.Setup] succeeds; the HCN namespace is attached +// and all endpoints are wired up inside the guest. +// - [StateInvalid]: entered when [Controller.Setup] fails mid-way; best-effort +// cleanup should be performed via [Controller.Teardown]. +// - [StateTornDown]: terminal state reached after [Controller.Teardown] completes. +// +// # Platform Variants +// +// Guest-side operations differ between LCOW and WCOW and are implemented in +// platform-specific source files selected via build tags +// (default for LCOW shim, "wcow" tag for WCOW shim). +package network diff --git a/internal/controller/network/interface.go b/internal/controller/network/interface.go new file mode 100644 index 0000000000..5b6302c8f4 --- /dev/null +++ b/internal/controller/network/interface.go @@ -0,0 +1,39 @@ +//go:build windows + +package network + +import ( + "context" + + "github.com/Microsoft/hcsshim/internal/gcs" +) + +// Controller manages the network lifecycle for a single pod running inside a UVM. +type Controller interface { + // Setup attaches the HCN namespace and its endpoints to the guest VM. + Setup(ctx context.Context, opts *SetupOptions) error + + // Teardown removes all guest-side NICs and the network namespace from the VM. + // It is idempotent: calling it on an already torn-down or unconfigured network is a no-op. + Teardown(ctx context.Context) error +} + +// SetupOptions holds the configuration required to set up the network for a pod. +type SetupOptions struct { + // PodID is the identifier of the pod whose network is being configured. + PodID string + + // NetworkNamespace is the HCN namespace ID to attach to the guest. + NetworkNamespace string + + // PolicyBasedRouting controls whether policy-based routing is configured + // for the endpoints added to the guest. Only relevant for LCOW. + PolicyBasedRouting bool +} + +// capabilitiesProvider is a narrow interface satisfied by guestmanager.Manager. +// It exists so callers pass the guest manager scoped only to Capabilities(), +// avoiding a hard dependency on the full guestmanager.Manager interface here. +type capabilitiesProvider interface { + Capabilities() gcs.GuestDefinedCapabilities +} diff --git a/internal/controller/network/network.go b/internal/controller/network/network.go new file mode 100644 index 0000000000..55d2e9c58a --- /dev/null +++ b/internal/controller/network/network.go @@ -0,0 +1,259 @@ +//go:build windows + +package network + +import ( + "context" + "errors" + "fmt" + "slices" + "strings" + "sync" + + "github.com/Microsoft/hcsshim/hcn" + "github.com/Microsoft/hcsshim/internal/log" + "github.com/Microsoft/hcsshim/internal/logfields" + "github.com/Microsoft/hcsshim/internal/vm/guestmanager" + "github.com/Microsoft/hcsshim/internal/vm/vmmanager" + + "github.com/Microsoft/go-winio/pkg/guid" + "github.com/sirupsen/logrus" +) + +// Manager is the concrete implementation of [Controller]. +type Manager struct { + mu sync.Mutex + + // podID is the identifier of the pod whose network this Controller manages. + podID string + + // namespaceID is the HCN namespace ID in use after a successful Setup. + namespaceID string + + // vmEndpoints maps nicID (ID within UVM) -> HCN endpoint. + vmEndpoints map[string]*hcn.HostComputeEndpoint + + // netState is the current lifecycle state of the network. + netState State + + // isNamespaceSupportedByGuest determines if network namespace is supported inside the guest + isNamespaceSupportedByGuest bool + + // vmNetManager performs host-side NIC hot-add/remove on the UVM. + vmNetManager vmmanager.NetworkManager + + // linuxGuestMgr performs guest-side NIC inject/remove for LCOW. + linuxGuestMgr guestmanager.LCOWNetworkManager + + // winGuestMgr performs guest-side NIC/namespace operations for WCOW. + winGuestMgr guestmanager.WCOWNetworkManager + + // capsProvider exposes the guest's declared capabilities. + // Used to check IsNamespaceAddRequestSupported. + capsProvider capabilitiesProvider +} + +// Assert that Manager implements Controller. +var _ Controller = (*Manager)(nil) + +// New creates a ready-to-use Manager in [StateNotConfigured]. +// +// This method is called from [VMController.CreateNetworkController()] +// which injects the necessary dependencies. +func New( + vmNetManager vmmanager.NetworkManager, + linuxGuestMgr guestmanager.LCOWNetworkManager, + windowsGuestMgr guestmanager.WCOWNetworkManager, + capsProvider capabilitiesProvider, +) *Manager { + m := &Manager{ + vmNetManager: vmNetManager, + linuxGuestMgr: linuxGuestMgr, + winGuestMgr: windowsGuestMgr, + capsProvider: capsProvider, + netState: StateNotConfigured, + vmEndpoints: make(map[string]*hcn.HostComputeEndpoint), + } + + // Cache once at construction so hot-add paths can branch without re-querying. + if caps := capsProvider.Capabilities(); caps != nil { + m.isNamespaceSupportedByGuest = caps.IsNamespaceAddRequestSupported() + } + + return m +} + +// Setup attaches the requested HCN namespace to the guest VM +// and hot-adds all endpoints found in that namespace. +// It must be called only once; subsequent calls return an error. +func (m *Manager) Setup(ctx context.Context, opts *SetupOptions) (err error) { + ctx, _ = log.WithContext(ctx, logrus.WithField(logfields.Operation, "Network Setup")) + + m.mu.Lock() + defer m.mu.Unlock() + + log.G(ctx).WithFields(logrus.Fields{ + logfields.PodID: opts.PodID, + logfields.Namespace: opts.NetworkNamespace, + }).Debug("starting network setup") + + // If Setup has already been called, then error out. + if m.netState != StateNotConfigured { + return fmt.Errorf("cannot set up network in state %s", m.netState) + } + + defer func() { + if err != nil { + // If setup fails for any reason, move to invalid so no further + // Setup calls are accepted. + m.netState = StateInvalid + log.G(ctx).WithError(err).Error("network setup failed, moving to invalid state") + } + }() + + if opts.NetworkNamespace == "" { + return fmt.Errorf("network namespace must not be empty") + } + + // Validate that the provided namespace exists. + hcnNamespace, err := hcn.GetNamespaceByID(opts.NetworkNamespace) + if err != nil { + return fmt.Errorf("get network namespace %s: %w", opts.NetworkNamespace, err) + } + + // Fetch all endpoints in the namespace. + endpoints, err := m.fetchEndpointsInNamespace(ctx, hcnNamespace) + if err != nil { + return fmt.Errorf("fetch endpoints in namespace %s: %w", hcnNamespace.Id, err) + } + + // Add the namespace to the guest. + if err = m.addNetNSInsideGuest(ctx, hcnNamespace); err != nil { + return fmt.Errorf("add network namespace to guest: %w", err) + } + + // Hot-add all endpoints in the namespace to the guest. + for _, endpoint := range endpoints { + nicGUID, err := guid.NewV4() + if err != nil { + return fmt.Errorf("generate NIC GUID: %w", err) + } + if err = m.addEndpointToGuestNamespace(ctx, nicGUID.String(), endpoint, opts.PolicyBasedRouting); err != nil { + return fmt.Errorf("add endpoint %s to guest: %w", endpoint.Name, err) + } + } + + m.podID = opts.PodID + m.namespaceID = hcnNamespace.Id + m.netState = StateConfigured + + log.G(ctx).WithFields(logrus.Fields{ + logfields.PodID: opts.PodID, + logfields.Namespace: hcnNamespace.Id, + }).Info("network setup completed successfully") + + return nil +} + +// Teardown removes all guest-side NICs and the HCN namespace from the UVM. +// +// It is idempotent: calling it when the network is already torn down or not yet +// configured is a no-op. +func (m *Manager) Teardown(ctx context.Context) error { + ctx, _ = log.WithContext(ctx, logrus.WithField(logfields.Operation, "Network Teardown")) + + m.mu.Lock() + defer m.mu.Unlock() + + log.G(ctx).WithFields(logrus.Fields{ + logfields.PodID: m.podID, + logfields.Namespace: m.namespaceID, + "State": m.netState, + }).Debug("starting network teardown") + + if m.netState == StateTornDown { + // Teardown is idempotent, so return nil if already torn down. + log.G(ctx).Info("network already torn down, skipping") + return nil + } + + if m.netState == StateNotConfigured { + // Nothing was configured; nothing to clean up. + log.G(ctx).Info("network not configured, skipping") + return nil + } + + // Remove all endpoints from the guest. + // Use a continue-on-error strategy: attempt every NIC regardless of individual + // failures, then collect all errors. + var teardownErrs []error + for nicID, endpoint := range m.vmEndpoints { + if err := m.removeEndpointFromGuestNamespace(ctx, nicID, endpoint); err != nil { + teardownErrs = append(teardownErrs, fmt.Errorf("remove endpoint %s from guest: %w", endpoint.Name, err)) + continue // continue attempting to remove other endpoints + } + + delete(m.vmEndpoints, nicID) + } + + if err := m.removeNetNSInsideGuest(ctx, m.namespaceID); err != nil { + teardownErrs = append(teardownErrs, fmt.Errorf("remove network namespace from guest: %w", err)) + } + + if len(teardownErrs) > 0 { + // If any errors were encountered during teardown, mark the state as invalid. + m.netState = StateInvalid + return errors.Join(teardownErrs...) + } + + // Mark as torn down if we do not encounter any errors. + // No further Setup or Teardown calls are allowed. + m.netState = StateTornDown + + log.G(ctx).WithFields(logrus.Fields{ + logfields.PodID: m.podID, + "networkNamespace": m.namespaceID, + }).Info("network teardown completed successfully") + + return nil +} + +// fetchEndpointsInNamespace retrieves all HCN endpoints present in +// the given namespace. +// Endpoints are sorted so that those with names ending in "eth0" appear first. +func (m *Manager) fetchEndpointsInNamespace(ctx context.Context, ns *hcn.HostComputeNamespace) ([]*hcn.HostComputeEndpoint, error) { + ctx, _ = log.WithContext(ctx, logrus.WithField(logfields.Namespace, ns.Id)) + log.G(ctx).Info("fetching endpoints from the network namespace") + + ids, err := hcn.GetNamespaceEndpointIds(ns.Id) + if err != nil { + return nil, fmt.Errorf("get endpoint IDs for namespace %s: %w", ns.Id, err) + } + endpoints := make([]*hcn.HostComputeEndpoint, 0, len(ids)) + for _, id := range ids { + ep, err := hcn.GetEndpointByID(id) + if err != nil { + return nil, fmt.Errorf("get endpoint %s: %w", id, err) + } + endpoints = append(endpoints, ep) + } + + // Ensure the endpoint named "eth0" is added first when multiple endpoints are present, + // so it maps to eth0 inside the guest. CNI results aren't available here, so we rely + // on the endpoint name suffix as a heuristic. + cmp := func(a, b *hcn.HostComputeEndpoint) int { + if strings.HasSuffix(a.Name, "eth0") { + return -1 + } + if strings.HasSuffix(b.Name, "eth0") { + return 1 + } + return 0 + } + + slices.SortStableFunc(endpoints, cmp) + + log.G(ctx).Tracef("fetched endpoints from the network namespace %+v", endpoints) + + return endpoints, nil +} diff --git a/internal/controller/network/network_lcow.go b/internal/controller/network/network_lcow.go new file mode 100644 index 0000000000..1fd73dc7fb --- /dev/null +++ b/internal/controller/network/network_lcow.go @@ -0,0 +1,97 @@ +//go:build windows && !wcow + +package network + +import ( + "context" + "fmt" + + "github.com/Microsoft/hcsshim/hcn" + hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" + "github.com/Microsoft/hcsshim/internal/log" + "github.com/Microsoft/hcsshim/internal/protocol/guestresource" + + "github.com/sirupsen/logrus" +) + +// addNetNSInsideGuest maps a host network namespace into the guest as a managed Guest Network Namespace. +// This is a no-op for LCOW as the network namespace is created via pause container +// and the adapters are added dynamically. +func (m *Manager) addNetNSInsideGuest(_ context.Context, _ *hcn.HostComputeNamespace) error { + return nil +} + +// removeNetNSInsideGuest is a no-op for LCOW; the guest-managed namespace +// is torn down automatically when pause container exits. +func (m *Manager) removeNetNSInsideGuest(_ context.Context, _ string) error { + return nil +} + +// addEndpointToGuestNamespace hot-adds an HCN endpoint to the UVM and, +// configures it inside the LCOW guest. +func (m *Manager) addEndpointToGuestNamespace(ctx context.Context, nicID string, endpoint *hcn.HostComputeEndpoint, isPolicyBasedRoutingSupported bool) error { + ctx, _ = log.WithContext(ctx, logrus.WithFields(logrus.Fields{"nicID": nicID, "endpointID": endpoint.Id})) + log.G(ctx).Info("adding endpoint to guest namespace") + + // 1. Host-side hot-add. + if err := m.vmNetManager.AddNIC(ctx, nicID, &hcsschema.NetworkAdapter{ + EndpointId: endpoint.Id, + MacAddress: endpoint.MacAddress, + }); err != nil { + return fmt.Errorf("add NIC %s to host (endpoint %s): %w", nicID, endpoint.Id, err) + } + + log.G(ctx).Debug("added NIC to host") + + // Track early so Teardown cleans up even if the guest Add call fails. + m.vmEndpoints[nicID] = endpoint + + // 2. Guest-side add. + if m.isNamespaceSupportedByGuest { + lcowAdapter, err := guestresource.BuildLCOWNetworkAdapter(nicID, endpoint, isPolicyBasedRoutingSupported) + if err != nil { + return fmt.Errorf("build LCOW network adapter for endpoint %s: %w", endpoint.Id, err) + } + + log.G(ctx).Tracef("built LCOW network adapter: %+v", lcowAdapter) + + if err := m.linuxGuestMgr.AddLCOWNetworkInterface(ctx, lcowAdapter); err != nil { + return fmt.Errorf("add NIC %s to guest (endpoint %s): %w", nicID, endpoint.Id, err) + } + + log.G(ctx).Debug("nic configured in guest") + } + + return nil +} + +// removeEndpointFromGuestNamespace removes an endpoint from the LCOW guest +// and then hot-removes the NIC from the host. +func (m *Manager) removeEndpointFromGuestNamespace(ctx context.Context, nicID string, endpoint *hcn.HostComputeEndpoint) error { + ctx, _ = log.WithContext(ctx, logrus.WithFields(logrus.Fields{"nicID": nicID, "endpointID": endpoint.Id})) + log.G(ctx).Info("removing endpoint from guest namespace") + + if m.isNamespaceSupportedByGuest { + // 1. LCOW guest-side removal. + if err := m.linuxGuestMgr.RemoveLCOWNetworkInterface(ctx, &guestresource.LCOWNetworkAdapter{ + NamespaceID: m.namespaceID, + ID: nicID, + }); err != nil { + return fmt.Errorf("remove NIC %s from guest: %w", nicID, err) + } + + log.G(ctx).Debug("removed NIC from guest") + } + + // 2. Host-side removal. + if err := m.vmNetManager.RemoveNIC(ctx, nicID, &hcsschema.NetworkAdapter{ + EndpointId: endpoint.Id, + MacAddress: endpoint.MacAddress, + }); err != nil { + return fmt.Errorf("remove NIC %s from host (endpoint %s): %w", nicID, endpoint.Id, err) + } + + log.G(ctx).Debug("removed NIC from host") + + return nil +} diff --git a/internal/controller/network/network_wcow.go b/internal/controller/network/network_wcow.go new file mode 100644 index 0000000000..f3a4dda84a --- /dev/null +++ b/internal/controller/network/network_wcow.go @@ -0,0 +1,130 @@ +//go:build windows && wcow + +package network + +import ( + "context" + "fmt" + + "github.com/Microsoft/hcsshim/hcn" + hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" + "github.com/Microsoft/hcsshim/internal/log" + "github.com/Microsoft/hcsshim/internal/logfields" + "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" + + "github.com/sirupsen/logrus" +) + +// addNetNSInsideGuest maps a host network namespace into the guest as a managed Guest Network Namespace. +// For WCOWs, this method sends a request to GCS for adding the namespace. +// GCS forwards the request to GNS which coordinates with HNS to add the namespace to the guest. +func (m *Manager) addNetNSInsideGuest(ctx context.Context, hcnNamespace *hcn.HostComputeNamespace) error { + ctx, _ = log.WithContext(ctx, logrus.WithField(logfields.Namespace, hcnNamespace.Id)) + + if m.isNamespaceSupportedByGuest { + log.G(ctx).Info("adding network namespace to guest") + + if err := m.winGuestMgr.AddNetworkNamespace(ctx, hcnNamespace); err != nil { + return fmt.Errorf("add network namespace %s to guest: %w", hcnNamespace.Id, err) + } + } + + return nil +} + +// removeNetNSInsideGuest removes the HCN namespace from the WCOW guest via GCS/GNS. +func (m *Manager) removeNetNSInsideGuest(ctx context.Context, namespaceID string) error { + ctx, _ = log.WithContext(ctx, logrus.WithField(logfields.Namespace, namespaceID)) + + if m.isNamespaceSupportedByGuest { + log.G(ctx).Info("removing network namespace from guest") + + hcnNamespace, err := hcn.GetNamespaceByID(namespaceID) + if err != nil { + return fmt.Errorf("get network namespace %s: %w", namespaceID, err) + } + + if err := m.winGuestMgr.RemoveNetworkNamespace(ctx, hcnNamespace); err != nil { + return fmt.Errorf("remove network namespace %s from guest: %w", namespaceID, err) + } + } + + return nil +} + +// addEndpointToGuestNamespace wires an HCN endpoint into the WCOW guest in three steps: +// pre-add (guest notification), host-side hot-add, and guest-side finalisation. +func (m *Manager) addEndpointToGuestNamespace(ctx context.Context, nicID string, endpoint *hcn.HostComputeEndpoint, _ bool) error { + ctx, _ = log.WithContext(ctx, logrus.WithFields(logrus.Fields{"nicID": nicID, "endpointID": endpoint.Id})) + log.G(ctx).Info("adding network endpoint to guest namespace") + + // 1. Guest pre-add: informs WCOW guest that a NIC is about to arrive. + if err := m.winGuestMgr.AddNetworkInterface( + ctx, + nicID, + guestrequest.RequestTypePreAdd, + endpoint, + ); err != nil { + return fmt.Errorf("pre-add NIC %s to guest (endpoint %s): %w", nicID, endpoint.Id, err) + } + + log.G(ctx).Info("pre-added network endpoint to guest namespace") + + // 2. Host-side hot-add. + if err := m.vmNetManager.AddNIC(ctx, nicID, &hcsschema.NetworkAdapter{ + EndpointId: endpoint.Id, + MacAddress: endpoint.MacAddress, + }); err != nil { + return fmt.Errorf("add NIC %s to host (endpoint %s): %w", nicID, endpoint.Id, err) + } + + log.G(ctx).Info("hot-added network endpoint to host") + + // Track early so Teardown cleans up even if the guest Add call fails. + m.vmEndpoints[nicID] = endpoint + + // 3. Guest add: finalise the NIC in the WCOW guest. + if err := m.winGuestMgr.AddNetworkInterface( + ctx, + nicID, + guestrequest.RequestTypeAdd, + nil, // No additional info is needed for the Add call. + ); err != nil { + return fmt.Errorf("add NIC %s to guest (endpoint %s): %w", nicID, endpoint.Id, err) + } + + log.G(ctx).Info("configured network endpoint in guest namespace") + + return nil +} + +// removeEndpointFromGuestNamespace removes an endpoint from the WCOW guest and then +// hot-removes the NIC from the host. +func (m *Manager) removeEndpointFromGuestNamespace(ctx context.Context, nicID string, endpoint *hcn.HostComputeEndpoint) error { + ctx, _ = log.WithContext(ctx, logrus.WithFields(logrus.Fields{"nicID": nicID, "endpointID": endpoint.Id})) + log.G(ctx).Info("removing network endpoint from guest namespace") + + // 1. Guest-side removal. + if err := m.winGuestMgr.RemoveNetworkInterface( + ctx, + nicID, + guestrequest.RequestTypeRemove, + nil, + ); err != nil { + return fmt.Errorf("remove NIC %s from guest (endpoint %s): %w", nicID, endpoint.Id, err) + } + + log.G(ctx).Info("removed network endpoint from guest namespace") + + // 2. Host-side removal. + if err := m.vmNetManager.RemoveNIC(ctx, nicID, &hcsschema.NetworkAdapter{ + EndpointId: endpoint.Id, + MacAddress: endpoint.MacAddress, + }); err != nil { + return fmt.Errorf("remove NIC %s from host (endpoint %s): %w", nicID, endpoint.Id, err) + } + + log.G(ctx).Info("removed network endpoint from host") + + return nil +} diff --git a/internal/controller/network/state.go b/internal/controller/network/state.go new file mode 100644 index 0000000000..1116b78753 --- /dev/null +++ b/internal/controller/network/state.go @@ -0,0 +1,66 @@ +//go:build windows + +package network + +// State represents the current lifecycle state of the network for a pod. +// +// The normal progression is: +// +// StateNotConfigured → StateConfigured → StateTornDown +// +// If an unrecoverable error occurs during [Controller.Setup], the network +// transitions to [StateInvalid] instead. +// A network in [StateInvalid] can only be cleaned up via [Controller.Teardown]. +// +// Full state-transition table: +// +// Current State │ Trigger │ Next State +// ────────────────────┼──────────────────┼────────────────── +// StateNotConfigured │ Setup succeeds │ StateConfigured +// StateNotConfigured │ Setup fails │ StateInvalid +// StateConfigured │ Teardown called │ StateTornDown +// StateInvalid │ Teardown called │ StateTornDown +// StateTornDown │ (terminal) │ — +type State int32 + +const ( + // StateNotConfigured is the initial state: no namespace has been attached + // and no NICs have been added. + // Valid transitions: + // - StateNotConfigured → StateConfigured (via [Controller.Setup], on success) + // - StateNotConfigured → StateInvalid (via [Controller.Setup], on failure) + StateNotConfigured State = iota + + // StateConfigured indicates the network is fully operational: the HCN namespace + // is attached, all endpoints are wired up, and guest-side NICs are hot-added. + // Valid transition: + // - StateConfigured → StateTornDown (via [Controller.Teardown]) + StateConfigured + + // StateInvalid indicates an unrecoverable error occurred during [Controller.Setup]. + // Teardown must be called to attempt best-effort cleanup. + // Valid transition: + // - StateInvalid → StateTornDown (via [Controller.Teardown]) + StateInvalid + + // StateTornDown is the terminal state reached after Teardown completes + // (regardless of whether Setup previously succeeded or failed). + // No further calls to Setup or Teardown are permitted. + StateTornDown +) + +// String returns a human-readable string representation of the network State. +func (s State) String() string { + switch s { + case StateNotConfigured: + return "NotConfigured" + case StateConfigured: + return "Configured" + case StateInvalid: + return "Invalid" + case StateTornDown: + return "TornDown" + default: + return "Unknown" + } +} diff --git a/internal/logfields/fields.go b/internal/logfields/fields.go index cceb3e2d18..6041013849 100644 --- a/internal/logfields/fields.go +++ b/internal/logfields/fields.go @@ -14,6 +14,7 @@ const ( ProcessID = "pid" TaskID = "tid" UVMID = "uvm-id" + PodID = "pod-id" // networking and IO diff --git a/internal/protocol/guestresource/parse.go b/internal/protocol/guestresource/parse.go new file mode 100644 index 0000000000..61e6683542 --- /dev/null +++ b/internal/protocol/guestresource/parse.go @@ -0,0 +1,78 @@ +//go:build windows + +package guestresource + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/Microsoft/hcsshim/hcn" + + "github.com/samber/lo" +) + +// BuildLCOWNetworkAdapter converts an HCN endpoint into the guestresource.LCOWNetworkAdapter +// payload that the GCS expects. +func BuildLCOWNetworkAdapter(nicID string, endpoint *hcn.HostComputeEndpoint, policyBasedRouting bool) (*LCOWNetworkAdapter, error) { + req := &LCOWNetworkAdapter{ + NamespaceID: endpoint.HostComputeNamespace, + ID: nicID, + MacAddress: endpoint.MacAddress, + IPConfigs: make([]LCOWIPConfig, 0, len(endpoint.IpConfigurations)), + Routes: make([]LCOWRoute, 0, len(endpoint.Routes)), + } + + for _, i := range endpoint.IpConfigurations { + ipConfig := LCOWIPConfig{ + IPAddress: i.IpAddress, + PrefixLength: i.PrefixLength, + } + req.IPConfigs = append(req.IPConfigs, ipConfig) + } + + for _, r := range endpoint.Routes { + newRoute := LCOWRoute{ + DestinationPrefix: r.DestinationPrefix, + NextHop: r.NextHop, + Metric: r.Metric, + } + req.Routes = append(req.Routes, newRoute) + } + + // !NOTE: + // the `DNSSuffix` field is explicitly used as the search list for host-name lookup in + // the guest's `resolv.conf`, and not as the DNS suffix. + // The name is a legacy hold over. + + // use DNS domain as the first (default) search value, if it is provided + searches := endpoint.Dns.Search + if endpoint.Dns.Domain != "" { + searches = append([]string{endpoint.Dns.Domain}, searches...) + } + + // canonicalize the DNS config + canon := func(s string, _ int) string { + // zone identifiers in IPv6 addresses really, really shouldn't be case sensitive, but ... *shrug* + return strings.ToLower(s) + } + servers := lo.Map(endpoint.Dns.ServerList, canon) + searches = lo.Map(searches, canon) + + req.DNSSuffix = strings.Join(searches, ",") + req.DNSServerList = strings.Join(servers, ",") + + for _, p := range endpoint.Policies { + if p.Type == hcn.EncapOverhead { + var settings hcn.EncapOverheadEndpointPolicySetting + if err := json.Unmarshal(p.Settings, &settings); err != nil { + return nil, fmt.Errorf("unmarshal encap overhead policy setting: %w", err) + } + req.EncapOverhead = settings.Overhead + } + } + + req.PolicyBasedRouting = policyBasedRouting + + return req, nil +} diff --git a/internal/uvm/network.go b/internal/uvm/network.go index 7836e1a7f1..6a00d84161 100644 --- a/internal/uvm/network.go +++ b/internal/uvm/network.go @@ -4,7 +4,6 @@ package uvm import ( "context" - "encoding/json" "fmt" "os" "slices" @@ -14,7 +13,6 @@ import ( "github.com/Microsoft/go-winio/pkg/guid" "github.com/containerd/ttrpc" "github.com/pkg/errors" - "github.com/samber/lo" "github.com/sirupsen/logrus" "github.com/Microsoft/hcsshim/hcn" @@ -554,71 +552,6 @@ func getNetworkModifyRequest(adapterID string, requestType guestrequest.RequestT } } -// convertToLCOWReq converts the HCN endpoint type to the guestresource.LCOWNetworkAdapter type that is -// passed to the GCS for a request. -func convertToLCOWReq(id string, endpoint *hcn.HostComputeEndpoint, policyBasedRouting bool) (*guestresource.LCOWNetworkAdapter, error) { - req := &guestresource.LCOWNetworkAdapter{ - NamespaceID: endpoint.HostComputeNamespace, - ID: id, - MacAddress: endpoint.MacAddress, - IPConfigs: make([]guestresource.LCOWIPConfig, 0, len(endpoint.IpConfigurations)), - Routes: make([]guestresource.LCOWRoute, 0, len(endpoint.Routes)), - } - - for _, i := range endpoint.IpConfigurations { - ipConfig := guestresource.LCOWIPConfig{ - IPAddress: i.IpAddress, - PrefixLength: i.PrefixLength, - } - req.IPConfigs = append(req.IPConfigs, ipConfig) - } - - for _, r := range endpoint.Routes { - newRoute := guestresource.LCOWRoute{ - DestinationPrefix: r.DestinationPrefix, - NextHop: r.NextHop, - Metric: r.Metric, - } - req.Routes = append(req.Routes, newRoute) - } - - // !NOTE: - // the `DNSSuffix` field is explicitly used as the search list for host-name lookup in - // the guest's `resolv.conf`, and not as the DNS suffix. - // The name is a legacy hold over. - - // use DNS domain as the first (default) search value, if it is provided - searches := endpoint.Dns.Search - if endpoint.Dns.Domain != "" { - searches = append([]string{endpoint.Dns.Domain}, searches...) - } - - // canonicalize the DNS config - canon := func(s string, _ int) string { - // zone identifiers in IPv6 addresses really, really shouldn't be case sensitive, but ... *shrug* - return strings.ToLower(s) - } - servers := lo.Map(endpoint.Dns.ServerList, canon) - searches = lo.Map(searches, canon) - - req.DNSSuffix = strings.Join(searches, ",") - req.DNSServerList = strings.Join(servers, ",") - - for _, p := range endpoint.Policies { - if p.Type == hcn.EncapOverhead { - var settings hcn.EncapOverheadEndpointPolicySetting - if err := json.Unmarshal(p.Settings, &settings); err != nil { - return nil, fmt.Errorf("unmarshal encap overhead policy setting: %w", err) - } - req.EncapOverhead = settings.Overhead - } - } - - req.PolicyBasedRouting = policyBasedRouting - - return req, nil -} - // addNIC adds a nic to the Utility VM. func (uvm *UtilityVM) addNIC(ctx context.Context, id string, endpoint *hcn.HostComputeEndpoint) error { // First a pre-add. This is a guest-only request and is only done on Windows. @@ -658,7 +591,7 @@ func (uvm *UtilityVM) addNIC(ctx context.Context, id string, endpoint *hcn.HostC nil), } } else { - s, err := convertToLCOWReq(id, endpoint, uvm.policyBasedRouting) + s, err := guestresource.BuildLCOWNetworkAdapter(id, endpoint, uvm.policyBasedRouting) if err != nil { return err }