Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,18 @@ jobs:
done
exit 1

- name: Test Windows guest control
- name: Test Windows lifecycle
run: |
make build-embedded
TEST_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
for attempt in 1 2 3; do
if sudo env \
"PATH=$TEST_PATH" \
"CI=true" \
"HYPEMAN_RUN_WINDOWS_GUEST_CONTROL_INTEGRATION=1" \
"HYPEMAN_RUN_WINDOWS_LIFECYCLE_INTEGRATION=1" \
"HYPEMAN_WINDOWS_OVMF_CODE=$HYPEMAN_WINDOWS_OVMF_CODE" \
"HYPEMAN_WINDOWS_OVMF_VARS=$HYPEMAN_WINDOWS_OVMF_VARS" \
go test -count=1 -run '^TestWindowsGuestAgentIntegration$' -timeout 2m ./lib/instances; then
go test -count=1 -run '^TestWindowsLifecycleIntegration$' -timeout 2m ./lib/instances; then
exit 0
fi
test "$attempt" = 3 || sleep 5
Expand Down
7 changes: 7 additions & 0 deletions docs/windows-networking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Windows networking

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here maybe


Hypeman can attach a Windows 11 QEMU guest to its normal TAP/bridge network. The public instance model remains unchanged: `NetworkEnabled` allocates the address, MAC, gateway, netmask, DNS servers, and TAP device used for Linux guests.

Create and start apply the current allocation before the instance becomes ready. A configuration failure fails the lifecycle operation rather than exposing a guest with partial networking.

RDP is not a Hypeman API. A prepared image may enable RDP, and callers can reach TCP port 3389 through the instance's generic allocated IP after applying their normal ingress policy.
2 changes: 2 additions & 0 deletions lib/guest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ type ReconfigureNetworkOptions struct {
IPv4 string
Prefix uint32
Gateway string
DNSServers []string
WaitForAgent time.Duration
}

Expand Down Expand Up @@ -240,6 +241,7 @@ func reconfigureNetworkOnce(ctx context.Context, dialer hypervisor.VsockDialer,
Ipv4: opts.IPv4,
Prefix: opts.Prefix,
Gateway: opts.Gateway,
DnsServers: opts.DNSServers,
})
finishGuestNetworkStepSpan(span, err)
if err != nil {
Expand Down
14 changes: 12 additions & 2 deletions lib/guest/guest.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/guest/guest.proto
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ message ReconfigureNetworkRequest {
string ipv4 = 3; // New IPv4 address without prefix
uint32 prefix = 4; // IPv4 prefix length
string gateway = 5; // Default gateway IPv4 address
repeated string dns_servers = 6; // DNS server IPv4 addresses
}

// ReconfigureNetworkResponse acknowledges the network reconfiguration request
Expand Down
6 changes: 6 additions & 0 deletions lib/instances/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ The launchable Windows image already defines its virtual disk size, so instance

A Windows VM remains `Initializing` until its guest agent answers over VioSock. This avoids treating firmware completion as application readiness.

### Windows networking

Windows uses the same host-side TAP allocation as Linux. Once the guest agent is reachable, the manager sends the complete allocation through the typed `ReconfigureNetwork` RPC. The agent selects the virtio-net adapter by MAC address, replaces stale IPv4 addresses and default routes, and applies DNS through native Windows APIs. It never invokes the Linux shell-command fallback.

Create treats network configuration as part of readiness and tears down a VM if it fails. Start reapplies the current allocation because a stopped instance may receive a different address or MAC before its next boot.

### Why Config Disk? (configdisk.go)

**What:** Read-only erofs disk with instance configuration
Expand Down
14 changes: 14 additions & 0 deletions lib/instances/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,20 @@ func (m *manager) createInstance(
log.WarnContext(ctx, "failed to update metadata after VM start", "instance_id", id, "error", err)
}

if windows && netConfig != nil {
networkCtx, networkSpanEnd := m.startLifecycleStep(ctx, "configure_guest_network",
attribute.String("instance_id", id),
attribute.String("hypervisor", string(stored.HypervisorType)),
attribute.String("operation", "configure_guest_network"),
)
if err := reconfigureGuestNetworkConfig(networkCtx, stored, netConfig); err != nil {
networkSpanEnd(err)
_, _ = m.stopInstance(ctx, id)
return nil, fmt.Errorf("configure Windows guest network: %w", err)
}
networkSpanEnd(nil)
}

// Success - release cleanup stack (prevent cleanup)
cu.Release()

Expand Down
56 changes: 45 additions & 11 deletions lib/instances/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,26 +474,47 @@ func (m *manager) acquireRestoreSlot(ctx context.Context, hvType hypervisor.Type
}

func reconfigureGuestNetwork(ctx context.Context, stored *StoredMetadata, alloc *network.Allocation) error {
cfg, err := guestNetworkReconfigureConfig(alloc)
if alloc == nil {
return fmt.Errorf("missing network allocation")
}
return reconfigureGuestNetworkConfig(ctx, stored, &network.NetworkConfig{
IP: alloc.IP, MAC: alloc.MAC, Gateway: alloc.Gateway, Netmask: alloc.Netmask, DNS: alloc.DNS, TAPDevice: alloc.TAPDevice,
})
}

func reconfigureGuestNetworkConfig(ctx context.Context, stored *StoredMetadata, netConfig *network.NetworkConfig) error {
cfg, err := guestNetworkReconfigureConfig(netConfig)
if err != nil {
return err
}
if isWindowsPlatform(stored.Platform) {
cfg.dns, err = parseWindowsDNSServers(netConfig.DNS)
if err != nil {
return err
}
}

dialer, err := hypervisor.NewVsockDialer(stored.HypervisorType, stored.VsockSocket, stored.VsockCID)
if err != nil {
return fmt.Errorf("create vsock dialer: %w", err)
}

interfaceName := "eth0"
if isWindowsPlatform(stored.Platform) {
interfaceName = ""
}
err = guest.ReconfigureNetworkInInstance(ctx, dialer, guest.ReconfigureNetworkOptions{
InterfaceName: "eth0",
InterfaceName: interfaceName,
MAC: cfg.mac,
IPv4: cfg.ip,
Prefix: uint32(cfg.prefix),
Gateway: cfg.gateway,
DNSServers: cfg.dns,
WaitForAgent: 120 * time.Second,
})
if err != nil {
if status.Code(err) == codes.Unimplemented {
if status.Code(err) == codes.Unimplemented && !isWindowsPlatform(stored.Platform) {
alloc := &network.Allocation{IP: netConfig.IP, MAC: netConfig.MAC, Gateway: netConfig.Gateway, Netmask: netConfig.Netmask}
return reconfigureGuestNetworkWithExec(ctx, dialer, alloc)
}
return fmt.Errorf("reconfigure guest network: %w", err)
Expand Down Expand Up @@ -528,37 +549,50 @@ type guestNetworkConfig struct {
ip string
mac string
gateway string
dns []string
prefix int
}

func guestNetworkReconfigureConfig(alloc *network.Allocation) (*guestNetworkConfig, error) {
if alloc == nil {
func guestNetworkReconfigureConfig(netConfig *network.NetworkConfig) (*guestNetworkConfig, error) {
if netConfig == nil {
return nil, fmt.Errorf("missing network allocation")
}
ip := strings.TrimSpace(alloc.IP)
ip := strings.TrimSpace(netConfig.IP)
if ip == "" {
return nil, fmt.Errorf("missing network allocation IP")
}
mac := strings.ToLower(strings.TrimSpace(alloc.MAC))
mac := strings.ToLower(strings.TrimSpace(netConfig.MAC))
if mac == "" {
return nil, fmt.Errorf("missing network allocation MAC")
}
if _, err := net.ParseMAC(mac); err != nil {
return nil, fmt.Errorf("invalid network allocation MAC %q: %w", alloc.MAC, err)
return nil, fmt.Errorf("invalid network allocation MAC %q: %w", netConfig.MAC, err)
}
gateway := strings.TrimSpace(alloc.Gateway)
gateway := strings.TrimSpace(netConfig.Gateway)
if gateway == "" {
return nil, fmt.Errorf("missing network allocation gateway")
}
prefix, err := netmaskToPrefix(alloc.Netmask)
prefix, err := netmaskToPrefix(netConfig.Netmask)
if err != nil {
return nil, err
}
return &guestNetworkConfig{ip: ip, mac: mac, gateway: gateway, prefix: prefix}, nil
}

func parseWindowsDNSServers(value string) ([]string, error) {
var dns []string
for _, server := range strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == ' ' }) {
server = strings.TrimSpace(server)
if net.ParseIP(server).To4() == nil {
return nil, fmt.Errorf("invalid Windows DNS server %q", server)
}
dns = append(dns, server)
}
return dns, nil
}

func guestNetworkReconfigureCommand(alloc *network.Allocation) (string, error) {
cfg, err := guestNetworkReconfigureConfig(alloc)
cfg, err := guestNetworkReconfigureConfig(networkConfigFromAllocation(alloc))
if err != nil {
return "", err
}
Expand Down
16 changes: 16 additions & 0 deletions lib/instances/restore_egress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ func TestNetworkConfigFromAllocation_PreservesDNS(t *testing.T) {
assert.Equal(t, alloc.TAPDevice, cfg.TAPDevice)
}

func TestWindowsDNSServerParsingDoesNotAffectLinuxConfig(t *testing.T) {
t.Parallel()

dns, err := parseWindowsDNSServers("1.1.1.1, 8.8.8.8")
require.NoError(t, err)
assert.Equal(t, []string{"1.1.1.1", "8.8.8.8"}, dns)
_, err = parseWindowsDNSServers("2606:4700:4700::1111")
require.ErrorContains(t, err, "invalid Windows DNS server")

cfg, err := guestNetworkReconfigureConfig(&network.NetworkConfig{
IP: "10.102.146.62", MAC: "02:00:00:85:17:c8", Gateway: "10.102.0.1", Netmask: "255.255.0.0", DNS: "2606:4700:4700::1111",
})
require.NoError(t, err)
assert.Empty(t, cfg.dns)
}

func TestGuestNetworkReconfigureCommand_AppliesAllocatedMAC(t *testing.T) {
t.Parallel()

Expand Down
14 changes: 14 additions & 0 deletions lib/instances/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,20 @@ func (m *manager) startInstance(
log.WarnContext(ctx, "failed to update metadata after VM start", "instance_id", id, "error", err)
}

if isWindowsPlatform(stored.Platform) && netConfig != nil {
networkCtx, networkSpanEnd := m.startLifecycleStep(ctx, "configure_guest_network",
attribute.String("instance_id", id),
attribute.String("hypervisor", string(stored.HypervisorType)),
attribute.String("operation", "configure_guest_network"),
)
if err := reconfigureGuestNetworkConfig(networkCtx, stored, netConfig); err != nil {
networkSpanEnd(err)
_, _ = m.stopInstance(ctx, id)
return nil, fmt.Errorf("configure Windows guest network: %w", err)
}
networkSpanEnd(nil)
}

// Return instance state from current metadata without forcing a log scan.
finalInst := m.toInstanceWithoutHydration(ctx, meta)
// Record metrics
Expand Down
3 changes: 0 additions & 3 deletions lib/instances/windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ func validateWindowsCreate(req CreateInstanceRequest, image *images.Image, caps
if req.Vcpus != 0 && req.Vcpus < 2 {
return fmt.Errorf("%w: Windows 11 requires at least 2 vCPUs", ErrInvalidRequest)
}
if req.NetworkEnabled {
return fmt.Errorf("%w: Windows networking is added in the networking phase", ErrInvalidRequest)
}
if len(req.Volumes) != 0 || len(req.Devices) != 0 || req.GPU != nil {
return fmt.Errorf("%w: Windows instances do not yet support volumes or device passthrough", ErrInvalidRequest)
}
Expand Down
Loading
Loading