diff --git a/lib/guest/client.go b/lib/guest/client.go index cb772dbc..f46b1149 100644 --- a/lib/guest/client.go +++ b/lib/guest/client.go @@ -82,8 +82,12 @@ func GetOrCreateConn(ctx context.Context, dialer hypervisor.VsockDialer) (*grpc. } // Create new connection using the VsockDialer + traceCtx := ctx conn, err := grpc.Dial("passthrough:///vsock", grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { + if span := trace.SpanFromContext(traceCtx); span.SpanContext().IsValid() { + ctx = trace.ContextWithSpan(ctx, span) + } netConn, err := dialer.DialVsock(ctx, vsockGuestPort) if err != nil { return nil, &AgentVSockDialError{Err: err} @@ -146,6 +150,109 @@ type ExecOptions struct { ResizeChan <-chan *WindowSize // Optional: channel to receive resize events (pointer to avoid copying mutex) } +type ReconfigureNetworkOptions struct { + InterfaceName string + MAC string + IPv4 string + Prefix uint32 + Gateway string + WaitForAgent time.Duration +} + +func ReconfigureNetworkInInstance(ctx context.Context, dialer hypervisor.VsockDialer, opts ReconfigureNetworkOptions) error { + if opts.WaitForAgent == 0 { + return reconfigureNetworkOnce(ctx, dialer, opts) + } + + ctx, span := otel.Tracer("hypeman/guest").Start(ctx, "guest.reconfigure_network", trace.WithAttributes( + attribute.Bool("wait_for_agent", true), + attribute.Int64("wait_for_agent_ms", opts.WaitForAgent.Milliseconds()), + )) + defer span.End() + + deadline := time.Now().Add(opts.WaitForAgent) + start := time.Now() + attempts := 0 + retryableAttempts := 0 + firstRetryableErrorType := "" + lastRetryableErrorType := "" + lastRetryInterval := time.Duration(0) + + for { + attempts++ + err := reconfigureNetworkOnce(ctx, dialer, opts) + if err == nil { + recordGuestExecWait(span, start, attempts, retryableAttempts, firstRetryableErrorType, lastRetryableErrorType, lastRetryInterval) + span.SetStatus(otelcodes.Ok, "") + return nil + } + if !isRetryableConnectionError(err) { + recordGuestExecWait(span, start, attempts, retryableAttempts, firstRetryableErrorType, lastRetryableErrorType, lastRetryInterval) + span.RecordError(err) + span.SetStatus(otelcodes.Error, err.Error()) + return err + } + + retryableAttempts++ + errType := retryableConnectionErrorType(err) + if firstRetryableErrorType == "" { + firstRetryableErrorType = errType + } + lastRetryableErrorType = errType + CloseConn(dialer.Key()) + + if time.Now().After(deadline) { + recordGuestExecWait(span, start, attempts, retryableAttempts, firstRetryableErrorType, lastRetryableErrorType, lastRetryInterval) + span.RecordError(err) + span.SetStatus(otelcodes.Error, err.Error()) + return err + } + + retryInterval := guestExecRetryInterval(time.Since(start)) + lastRetryInterval = retryInterval + select { + case <-ctx.Done(): + recordGuestExecWait(span, start, attempts, retryableAttempts, firstRetryableErrorType, lastRetryableErrorType, lastRetryInterval) + span.RecordError(ctx.Err()) + span.SetStatus(otelcodes.Error, ctx.Err().Error()) + return ctx.Err() + case <-time.After(retryInterval): + } + } +} + +func reconfigureNetworkOnce(ctx context.Context, dialer hypervisor.VsockDialer, opts ReconfigureNetworkOptions) error { + grpcConn, err := GetOrCreateConn(ctx, dialer) + if err != nil { + return fmt.Errorf("get grpc connection: %w", err) + } + client := NewGuestServiceClient(grpcConn) + + _, span := otel.Tracer("hypeman/guest").Start(ctx, "guest.reconfigure_network.rpc") + _, err = client.ReconfigureNetwork(ctx, &ReconfigureNetworkRequest{ + InterfaceName: opts.InterfaceName, + Mac: opts.MAC, + Ipv4: opts.IPv4, + Prefix: opts.Prefix, + Gateway: opts.Gateway, + }) + finishGuestNetworkStepSpan(span, err) + if err != nil { + return fmt.Errorf("reconfigure network rpc: %w", err) + } + return nil +} + +func finishGuestNetworkStepSpan(span trace.Span, err error) { + if err != nil { + span.RecordError(err) + span.SetStatus(otelcodes.Error, err.Error()) + } else { + span.SetStatus(otelcodes.Ok, "") + } + span.End() +} + // ExecIntoInstance executes command in instance via vsock using gRPC. // The dialer is a hypervisor-specific VsockDialer that knows how to connect to the guest. // If WaitForAgent is set, it will retry on connection errors until the timeout. diff --git a/lib/guest/guest.pb.go b/lib/guest/guest.pb.go index 0de1628e..a239fc97 100644 --- a/lib/guest/guest.pb.go +++ b/lib/guest/guest.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v3.21.12 +// protoc v6.33.4 // source: lib/guest/guest.proto package guest @@ -1266,6 +1266,120 @@ func (*ShutdownResponse) Descriptor() ([]byte, []int) { return file_lib_guest_guest_proto_rawDescGZIP(), []int{16} } +// ReconfigureNetworkRequest updates a guest network interface after snapshot restore +type ReconfigureNetworkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + InterfaceName string `protobuf:"bytes,1,opt,name=interface_name,json=interfaceName,proto3" json:"interface_name,omitempty"` // Interface to reconfigure, defaults to eth0 + Mac string `protobuf:"bytes,2,opt,name=mac,proto3" json:"mac,omitempty"` // New MAC address + Ipv4 string `protobuf:"bytes,3,opt,name=ipv4,proto3" json:"ipv4,omitempty"` // New IPv4 address without prefix + Prefix uint32 `protobuf:"varint,4,opt,name=prefix,proto3" json:"prefix,omitempty"` // IPv4 prefix length + Gateway string `protobuf:"bytes,5,opt,name=gateway,proto3" json:"gateway,omitempty"` // Default gateway IPv4 address + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReconfigureNetworkRequest) Reset() { + *x = ReconfigureNetworkRequest{} + mi := &file_lib_guest_guest_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReconfigureNetworkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReconfigureNetworkRequest) ProtoMessage() {} + +func (x *ReconfigureNetworkRequest) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReconfigureNetworkRequest.ProtoReflect.Descriptor instead. +func (*ReconfigureNetworkRequest) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{17} +} + +func (x *ReconfigureNetworkRequest) GetInterfaceName() string { + if x != nil { + return x.InterfaceName + } + return "" +} + +func (x *ReconfigureNetworkRequest) GetMac() string { + if x != nil { + return x.Mac + } + return "" +} + +func (x *ReconfigureNetworkRequest) GetIpv4() string { + if x != nil { + return x.Ipv4 + } + return "" +} + +func (x *ReconfigureNetworkRequest) GetPrefix() uint32 { + if x != nil { + return x.Prefix + } + return 0 +} + +func (x *ReconfigureNetworkRequest) GetGateway() string { + if x != nil { + return x.Gateway + } + return "" +} + +// ReconfigureNetworkResponse acknowledges the network reconfiguration request +type ReconfigureNetworkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReconfigureNetworkResponse) Reset() { + *x = ReconfigureNetworkResponse{} + mi := &file_lib_guest_guest_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReconfigureNetworkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReconfigureNetworkResponse) ProtoMessage() {} + +func (x *ReconfigureNetworkResponse) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReconfigureNetworkResponse.ProtoReflect.Descriptor instead. +func (*ReconfigureNetworkResponse) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{18} +} + var File_lib_guest_guest_proto protoreflect.FileDescriptor const file_lib_guest_guest_proto_rawDesc = "" + @@ -1358,13 +1472,21 @@ const file_lib_guest_guest_proto_rawDesc = "" + "\x05error\x18\b \x01(\tR\x05error\")\n" + "\x0fShutdownRequest\x12\x16\n" + "\x06signal\x18\x01 \x01(\x05R\x06signal\"\x12\n" + - "\x10ShutdownResponse2\xd3\x02\n" + + "\x10ShutdownResponse\"\x9a\x01\n" + + "\x19ReconfigureNetworkRequest\x12%\n" + + "\x0einterface_name\x18\x01 \x01(\tR\rinterfaceName\x12\x10\n" + + "\x03mac\x18\x02 \x01(\tR\x03mac\x12\x12\n" + + "\x04ipv4\x18\x03 \x01(\tR\x04ipv4\x12\x16\n" + + "\x06prefix\x18\x04 \x01(\rR\x06prefix\x12\x18\n" + + "\agateway\x18\x05 \x01(\tR\agateway\"\x1c\n" + + "\x1aReconfigureNetworkResponse2\xae\x03\n" + "\fGuestService\x123\n" + "\x04Exec\x12\x12.guest.ExecRequest\x1a\x13.guest.ExecResponse(\x010\x01\x12F\n" + "\vCopyToGuest\x12\x19.guest.CopyToGuestRequest\x1a\x1a.guest.CopyToGuestResponse(\x01\x12L\n" + "\rCopyFromGuest\x12\x1b.guest.CopyFromGuestRequest\x1a\x1c.guest.CopyFromGuestResponse0\x01\x12;\n" + "\bStatPath\x12\x16.guest.StatPathRequest\x1a\x17.guest.StatPathResponse\x12;\n" + - "\bShutdown\x12\x16.guest.ShutdownRequest\x1a\x17.guest.ShutdownResponseB'Z%github.com/onkernel/hypeman/lib/guestb\x06proto3" + "\bShutdown\x12\x16.guest.ShutdownRequest\x1a\x17.guest.ShutdownResponse\x12Y\n" + + "\x12ReconfigureNetwork\x12 .guest.ReconfigureNetworkRequest\x1a!.guest.ReconfigureNetworkResponseB'Z%github.com/onkernel/hypeman/lib/guestb\x06proto3" var ( file_lib_guest_guest_proto_rawDescOnce sync.Once @@ -1378,31 +1500,33 @@ func file_lib_guest_guest_proto_rawDescGZIP() []byte { return file_lib_guest_guest_proto_rawDescData } -var file_lib_guest_guest_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_lib_guest_guest_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_lib_guest_guest_proto_goTypes = []any{ - (*ExecRequest)(nil), // 0: guest.ExecRequest - (*ExecStart)(nil), // 1: guest.ExecStart - (*WindowSize)(nil), // 2: guest.WindowSize - (*ExecResponse)(nil), // 3: guest.ExecResponse - (*CopyToGuestRequest)(nil), // 4: guest.CopyToGuestRequest - (*CopyToGuestStart)(nil), // 5: guest.CopyToGuestStart - (*CopyToGuestEnd)(nil), // 6: guest.CopyToGuestEnd - (*CopyToGuestResponse)(nil), // 7: guest.CopyToGuestResponse - (*CopyFromGuestRequest)(nil), // 8: guest.CopyFromGuestRequest - (*CopyFromGuestResponse)(nil), // 9: guest.CopyFromGuestResponse - (*CopyFromGuestHeader)(nil), // 10: guest.CopyFromGuestHeader - (*CopyFromGuestEnd)(nil), // 11: guest.CopyFromGuestEnd - (*CopyFromGuestError)(nil), // 12: guest.CopyFromGuestError - (*StatPathRequest)(nil), // 13: guest.StatPathRequest - (*StatPathResponse)(nil), // 14: guest.StatPathResponse - (*ShutdownRequest)(nil), // 15: guest.ShutdownRequest - (*ShutdownResponse)(nil), // 16: guest.ShutdownResponse - nil, // 17: guest.ExecStart.EnvEntry + (*ExecRequest)(nil), // 0: guest.ExecRequest + (*ExecStart)(nil), // 1: guest.ExecStart + (*WindowSize)(nil), // 2: guest.WindowSize + (*ExecResponse)(nil), // 3: guest.ExecResponse + (*CopyToGuestRequest)(nil), // 4: guest.CopyToGuestRequest + (*CopyToGuestStart)(nil), // 5: guest.CopyToGuestStart + (*CopyToGuestEnd)(nil), // 6: guest.CopyToGuestEnd + (*CopyToGuestResponse)(nil), // 7: guest.CopyToGuestResponse + (*CopyFromGuestRequest)(nil), // 8: guest.CopyFromGuestRequest + (*CopyFromGuestResponse)(nil), // 9: guest.CopyFromGuestResponse + (*CopyFromGuestHeader)(nil), // 10: guest.CopyFromGuestHeader + (*CopyFromGuestEnd)(nil), // 11: guest.CopyFromGuestEnd + (*CopyFromGuestError)(nil), // 12: guest.CopyFromGuestError + (*StatPathRequest)(nil), // 13: guest.StatPathRequest + (*StatPathResponse)(nil), // 14: guest.StatPathResponse + (*ShutdownRequest)(nil), // 15: guest.ShutdownRequest + (*ShutdownResponse)(nil), // 16: guest.ShutdownResponse + (*ReconfigureNetworkRequest)(nil), // 17: guest.ReconfigureNetworkRequest + (*ReconfigureNetworkResponse)(nil), // 18: guest.ReconfigureNetworkResponse + nil, // 19: guest.ExecStart.EnvEntry } var file_lib_guest_guest_proto_depIdxs = []int32{ 1, // 0: guest.ExecRequest.start:type_name -> guest.ExecStart 2, // 1: guest.ExecRequest.resize:type_name -> guest.WindowSize - 17, // 2: guest.ExecStart.env:type_name -> guest.ExecStart.EnvEntry + 19, // 2: guest.ExecStart.env:type_name -> guest.ExecStart.EnvEntry 5, // 3: guest.CopyToGuestRequest.start:type_name -> guest.CopyToGuestStart 6, // 4: guest.CopyToGuestRequest.end:type_name -> guest.CopyToGuestEnd 10, // 5: guest.CopyFromGuestResponse.header:type_name -> guest.CopyFromGuestHeader @@ -1413,13 +1537,15 @@ var file_lib_guest_guest_proto_depIdxs = []int32{ 8, // 10: guest.GuestService.CopyFromGuest:input_type -> guest.CopyFromGuestRequest 13, // 11: guest.GuestService.StatPath:input_type -> guest.StatPathRequest 15, // 12: guest.GuestService.Shutdown:input_type -> guest.ShutdownRequest - 3, // 13: guest.GuestService.Exec:output_type -> guest.ExecResponse - 7, // 14: guest.GuestService.CopyToGuest:output_type -> guest.CopyToGuestResponse - 9, // 15: guest.GuestService.CopyFromGuest:output_type -> guest.CopyFromGuestResponse - 14, // 16: guest.GuestService.StatPath:output_type -> guest.StatPathResponse - 16, // 17: guest.GuestService.Shutdown:output_type -> guest.ShutdownResponse - 13, // [13:18] is the sub-list for method output_type - 8, // [8:13] is the sub-list for method input_type + 17, // 13: guest.GuestService.ReconfigureNetwork:input_type -> guest.ReconfigureNetworkRequest + 3, // 14: guest.GuestService.Exec:output_type -> guest.ExecResponse + 7, // 15: guest.GuestService.CopyToGuest:output_type -> guest.CopyToGuestResponse + 9, // 16: guest.GuestService.CopyFromGuest:output_type -> guest.CopyFromGuestResponse + 14, // 17: guest.GuestService.StatPath:output_type -> guest.StatPathResponse + 16, // 18: guest.GuestService.Shutdown:output_type -> guest.ShutdownResponse + 18, // 19: guest.GuestService.ReconfigureNetwork:output_type -> guest.ReconfigureNetworkResponse + 14, // [14:20] is the sub-list for method output_type + 8, // [8:14] is the sub-list for method input_type 8, // [8:8] is the sub-list for extension type_name 8, // [8:8] is the sub-list for extension extendee 0, // [0:8] is the sub-list for field type_name @@ -1457,7 +1583,7 @@ func file_lib_guest_guest_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_lib_guest_guest_proto_rawDesc), len(file_lib_guest_guest_proto_rawDesc)), NumEnums: 0, - NumMessages: 18, + NumMessages: 20, NumExtensions: 0, NumServices: 1, }, diff --git a/lib/guest/guest.proto b/lib/guest/guest.proto index c42198a9..317c21b3 100644 --- a/lib/guest/guest.proto +++ b/lib/guest/guest.proto @@ -20,6 +20,9 @@ service GuestService { // Shutdown requests graceful VM shutdown by signaling init (PID 1) rpc Shutdown(ShutdownRequest) returns (ShutdownResponse); + + // ReconfigureNetwork updates the guest network identity without spawning shell commands + rpc ReconfigureNetwork(ReconfigureNetworkRequest) returns (ReconfigureNetworkResponse); } // ExecRequest represents messages from client to server @@ -154,3 +157,15 @@ message ShutdownRequest { // ShutdownResponse acknowledges the shutdown request message ShutdownResponse {} + +// ReconfigureNetworkRequest updates a guest network interface after snapshot restore +message ReconfigureNetworkRequest { + string interface_name = 1; // Interface to reconfigure, defaults to eth0 + string mac = 2; // New MAC address + string ipv4 = 3; // New IPv4 address without prefix + uint32 prefix = 4; // IPv4 prefix length + string gateway = 5; // Default gateway IPv4 address +} + +// ReconfigureNetworkResponse acknowledges the network reconfiguration request +message ReconfigureNetworkResponse {} diff --git a/lib/guest/guest_grpc.pb.go b/lib/guest/guest_grpc.pb.go index 71224327..f93631d9 100644 --- a/lib/guest/guest_grpc.pb.go +++ b/lib/guest/guest_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.0 -// - protoc v3.21.12 +// - protoc v6.33.4 // source: lib/guest/guest.proto package guest @@ -19,11 +19,12 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - GuestService_Exec_FullMethodName = "/guest.GuestService/Exec" - GuestService_CopyToGuest_FullMethodName = "/guest.GuestService/CopyToGuest" - GuestService_CopyFromGuest_FullMethodName = "/guest.GuestService/CopyFromGuest" - GuestService_StatPath_FullMethodName = "/guest.GuestService/StatPath" - GuestService_Shutdown_FullMethodName = "/guest.GuestService/Shutdown" + GuestService_Exec_FullMethodName = "/guest.GuestService/Exec" + GuestService_CopyToGuest_FullMethodName = "/guest.GuestService/CopyToGuest" + GuestService_CopyFromGuest_FullMethodName = "/guest.GuestService/CopyFromGuest" + GuestService_StatPath_FullMethodName = "/guest.GuestService/StatPath" + GuestService_Shutdown_FullMethodName = "/guest.GuestService/Shutdown" + GuestService_ReconfigureNetwork_FullMethodName = "/guest.GuestService/ReconfigureNetwork" ) // GuestServiceClient is the client API for GuestService service. @@ -42,6 +43,8 @@ type GuestServiceClient interface { StatPath(ctx context.Context, in *StatPathRequest, opts ...grpc.CallOption) (*StatPathResponse, error) // Shutdown requests graceful VM shutdown by signaling init (PID 1) Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error) + // ReconfigureNetwork updates the guest network identity without spawning shell commands + ReconfigureNetwork(ctx context.Context, in *ReconfigureNetworkRequest, opts ...grpc.CallOption) (*ReconfigureNetworkResponse, error) } type guestServiceClient struct { @@ -117,6 +120,16 @@ func (c *guestServiceClient) Shutdown(ctx context.Context, in *ShutdownRequest, return out, nil } +func (c *guestServiceClient) ReconfigureNetwork(ctx context.Context, in *ReconfigureNetworkRequest, opts ...grpc.CallOption) (*ReconfigureNetworkResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReconfigureNetworkResponse) + err := c.cc.Invoke(ctx, GuestService_ReconfigureNetwork_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // GuestServiceServer is the server API for GuestService service. // All implementations must embed UnimplementedGuestServiceServer // for forward compatibility. @@ -133,6 +146,8 @@ type GuestServiceServer interface { StatPath(context.Context, *StatPathRequest) (*StatPathResponse, error) // Shutdown requests graceful VM shutdown by signaling init (PID 1) Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error) + // ReconfigureNetwork updates the guest network identity without spawning shell commands + ReconfigureNetwork(context.Context, *ReconfigureNetworkRequest) (*ReconfigureNetworkResponse, error) mustEmbedUnimplementedGuestServiceServer() } @@ -158,6 +173,9 @@ func (UnimplementedGuestServiceServer) StatPath(context.Context, *StatPathReques func (UnimplementedGuestServiceServer) Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error) { return nil, status.Error(codes.Unimplemented, "method Shutdown not implemented") } +func (UnimplementedGuestServiceServer) ReconfigureNetwork(context.Context, *ReconfigureNetworkRequest) (*ReconfigureNetworkResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReconfigureNetwork not implemented") +} func (UnimplementedGuestServiceServer) mustEmbedUnimplementedGuestServiceServer() {} func (UnimplementedGuestServiceServer) testEmbeddedByValue() {} @@ -240,6 +258,24 @@ func _GuestService_Shutdown_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _GuestService_ReconfigureNetwork_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReconfigureNetworkRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GuestServiceServer).ReconfigureNetwork(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GuestService_ReconfigureNetwork_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GuestServiceServer).ReconfigureNetwork(ctx, req.(*ReconfigureNetworkRequest)) + } + return interceptor(ctx, in, info, handler) +} + // GuestService_ServiceDesc is the grpc.ServiceDesc for GuestService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -255,6 +291,10 @@ var GuestService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Shutdown", Handler: _GuestService_Shutdown_Handler, }, + { + MethodName: "ReconfigureNetwork", + Handler: _GuestService_ReconfigureNetwork_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/lib/hypervisor/firecracker/vsock.go b/lib/hypervisor/firecracker/vsock.go index c2c84a2c..286cb1f6 100644 --- a/lib/hypervisor/firecracker/vsock.go +++ b/lib/hypervisor/firecracker/vsock.go @@ -9,6 +9,8 @@ import ( "time" "github.com/kernel/hypeman/lib/hypervisor" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" ) const ( @@ -32,46 +34,66 @@ func (d *VsockDialer) Key() string { return "firecracker:" + d.socketPath } -func (d *VsockDialer) DialVsock(ctx context.Context, port int) (net.Conn, error) { +func (d *VsockDialer) DialVsock(ctx context.Context, port int) (_ net.Conn, retErr error) { + ctx, span := hypervisor.StartImplementationSpan(ctx, hypervisor.TypeFirecracker, "hypervisor.vsock.dial", + attribute.Int("vsock.port", port), + ) + defer func() { hypervisor.FinishTraceSpan(span, retErr) }() + dialTimeout := vsockDialTimeout if deadline, ok := ctx.Deadline(); ok { if remaining := time.Until(deadline); remaining < dialTimeout { dialTimeout = remaining } } + span.SetAttributes(attribute.Int64("dial_timeout_ms", dialTimeout.Milliseconds())) dialer := net.Dialer{Timeout: dialTimeout} - conn, err := dialer.DialContext(ctx, "unix", d.socketPath) + stepCtx, stepSpan := otel.Tracer("hypeman/hypervisor/firecracker").Start(ctx, "hypervisor.vsock.unix_dial") + conn, err := dialer.DialContext(stepCtx, "unix", d.socketPath) + hypervisor.FinishTraceSpan(stepSpan, err) if err != nil { - return nil, fmt.Errorf("dial vsock socket %s: %w", d.socketPath, err) + retErr = fmt.Errorf("dial vsock socket %s: %w", d.socketPath, err) + return nil, retErr } if err := conn.SetDeadline(time.Now().Add(vsockHandshakeTimeout)); err != nil { _ = conn.Close() - return nil, fmt.Errorf("set handshake deadline: %w", err) + retErr = fmt.Errorf("set handshake deadline: %w", err) + return nil, retErr } + _, stepSpan = otel.Tracer("hypeman/hypervisor/firecracker").Start(ctx, "hypervisor.vsock.write_connect") if _, err := conn.Write([]byte(fmt.Sprintf("CONNECT %d\n", port))); err != nil { + hypervisor.FinishTraceSpan(stepSpan, err) _ = conn.Close() - return nil, fmt.Errorf("send vsock handshake: %w", err) + retErr = fmt.Errorf("send vsock handshake: %w", err) + return nil, retErr } + hypervisor.FinishTraceSpan(stepSpan, nil) reader := bufio.NewReader(conn) + _, stepSpan = otel.Tracer("hypeman/hypervisor/firecracker").Start(ctx, "hypervisor.vsock.read_ok") response, err := reader.ReadString('\n') if err != nil { + hypervisor.FinishTraceSpan(stepSpan, err) _ = conn.Close() - return nil, fmt.Errorf("read vsock handshake response (is exec-agent running in guest?): %w", err) + retErr = fmt.Errorf("read vsock handshake response (is exec-agent running in guest?): %w", err) + return nil, retErr } + hypervisor.FinishTraceSpan(stepSpan, nil) if err := conn.SetDeadline(time.Time{}); err != nil { _ = conn.Close() - return nil, fmt.Errorf("clear handshake deadline: %w", err) + retErr = fmt.Errorf("clear handshake deadline: %w", err) + return nil, retErr } response = strings.TrimSpace(response) if !strings.HasPrefix(response, "OK ") { _ = conn.Close() - return nil, fmt.Errorf("vsock handshake failed: %s", response) + retErr = fmt.Errorf("vsock handshake failed: %s", response) + return nil, retErr } return &bufferedConn{Conn: conn, reader: reader}, nil diff --git a/lib/instances/reconfigure_guest_network_perf_test.go b/lib/instances/reconfigure_guest_network_perf_test.go new file mode 100644 index 00000000..fcda012c --- /dev/null +++ b/lib/instances/reconfigure_guest_network_perf_test.go @@ -0,0 +1,272 @@ +//go:build linux + +package instances + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/paths" + snapshottest "github.com/kernel/hypeman/lib/snapshot/testsupport" + "github.com/kernel/hypeman/lib/system" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" +) + +const reconfigureGuestNetworkPerfEnv = "HYPEMAN_RUN_RECONFIGURE_GUEST_NETWORK_PERF" + +func TestReconfigureGuestNetworkPerf(t *testing.T) { + if os.Getenv(reconfigureGuestNetworkPerfEnv) != "1" { + t.Skipf("set %s=1 to run snapshot fork network reconfigure perf test", reconfigureGuestNetworkPerfEnv) + } + requireFirecrackerIntegrationPrereqs(t) + + recorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithSpanProcessor(recorder), + ) + previousProvider := otel.GetTracerProvider() + otel.SetTracerProvider(provider) + t.Cleanup(func() { + otel.SetTracerProvider(previousProvider) + _ = provider.Shutdown(context.Background()) + }) + + iterations := perfIterationsFromEnv(t, 5) + mgr, tmpDir := setupTestManagerForFirecracker(t) + ctx := context.Background() + p := paths.New(tmpDir) + + imageManager, err := images.NewManager(p, 1, nil) + require.NoError(t, err) + imageName := integrationTestImageRef(t, "docker.io/library/alpine:latest") + snapshottest.EnsureImageReady(t, ctx, p, imageManager, imageName) + + systemManager := system.NewManager(p) + require.NoError(t, systemManager.EnsureSystemFiles(ctx)) + require.NoError(t, mgr.networkManager.Initialize(ctx, nil)) + + source, err := mgr.CreateInstance(ctx, CreateInstanceRequest{ + Name: "fc-reconfigure-perf-src", + Image: imageName, + Size: 1024 * 1024 * 1024, + OverlaySize: 1024 * 1024 * 1024, + Vcpus: 1, + NetworkEnabled: true, + Hypervisor: hypervisor.TypeFirecracker, + Cmd: []string{"sleep", "infinity"}, + }) + require.NoError(t, err) + sourceID := source.Id + t.Cleanup(func() { _ = mgr.DeleteInstance(context.Background(), sourceID) }) + + source, err = waitForInstanceState(ctx, mgr, sourceID, StateRunning, integrationTestTimeout(45*time.Second)) + require.NoError(t, err) + require.NoError(t, waitForExecAgent(ctx, mgr, sourceID, 45*time.Second)) + + snapshot, err := mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{ + Kind: SnapshotKindStandby, + Name: "fc-reconfigure-perf-snap", + }) + require.NoError(t, err) + t.Cleanup(func() { _ = mgr.DeleteSnapshot(context.Background(), snapshot.Id) }) + + for i := 1; i <= iterations; i++ { + before := len(recorder.Ended()) + start := time.Now() + fork, err := mgr.ForkSnapshot(ctx, snapshot.Id, ForkSnapshotRequest{ + Name: fmt.Sprintf("fc-reconfigure-perf-fork-%02d", i), + TargetState: StateRunning, + }) + forkElapsed := time.Since(start) + require.NoError(t, err) + require.Equal(t, StateRunning, fork.State) + require.NoError(t, waitForExecAgent(ctx, mgr, fork.Id, 45*time.Second)) + + spans := append([]sdktrace.ReadOnlySpan(nil), recorder.Ended()[before:]...) + t.Log(formatReconfigurePerfLine(i, forkElapsed, spans)) + _ = mgr.DeleteInstance(ctx, fork.Id) + } +} + +func perfIterationsFromEnv(t *testing.T, fallback int) int { + t.Helper() + raw := strings.TrimSpace(os.Getenv("HYPEMAN_RECONFIGURE_GUEST_NETWORK_PERF_ITERS")) + if raw == "" { + return fallback + } + n, err := strconv.Atoi(raw) + require.NoError(t, err) + require.Positive(t, n) + return n +} + +func formatReconfigurePerfLine(iter int, forkElapsed time.Duration, spans []sdktrace.ReadOnlySpan) string { + reconfigure := lastSpanNamed(spans, "reconfigure_guest_network") + if reconfigure == nil { + return fmt.Sprintf("PERF iter=%d fork_total_ms=%d reconfigure_guest_network_ms=missing spans=%d", iter, forkElapsed.Milliseconds(), len(spans)) + } + + desc := descendantSpans(spans, reconfigure) + exec := lastSpanNamed(desc, "guest.exec") + networkRPC := lastSpanNamed(desc, "guest.reconfigure_network") + getConn := lastSpanNamed(desc, "guest.exec.get_conn") + openStream := lastSpanNamed(desc, "guest.exec.open_stream") + sendStart := lastSpanNamed(desc, "guest.exec.send_start") + recvUntilExit := lastSpanNamed(desc, "guest.exec.recv_until_exit") + vsockDial := lastSpanNamed(desc, "hypervisor.vsock.dial") + vsockUnixDial := lastSpanNamed(desc, "hypervisor.vsock.unix_dial") + vsockWriteConnect := lastSpanNamed(desc, "hypervisor.vsock.write_connect") + vsockReadOK := lastSpanNamed(desc, "hypervisor.vsock.read_ok") + + return fmt.Sprintf( + "PERF iter=%d fork_total_ms=%d reconfigure_guest_network_ms=%d guest_exec_ms=%d guest_network_rpc_ms=%d get_conn_ms=%d open_stream_ms=%d send_start_ms=%d recv_until_exit_ms=%d vsock_dial_ms=%d vsock_unix_dial_ms=%d vsock_write_connect_ms=%d vsock_read_ok_ms=%d attempts=%d retryable_attempts=%d network_rpc_attempts=%d network_rpc_retryable_attempts=%d first_retryable_error=%s last_retryable_error=%s wait_elapsed_ms=%d open_stream_attempts=%s recv_attempts=%s unary_attempts=%s vsock_dial_attempts=%s vsock_unix_dial_attempts=%s vsock_write_connect_attempts=%s vsock_read_ok_attempts=%s", + iter, + forkElapsed.Milliseconds(), + spanDurationMS(reconfigure), + spanDurationMS(exec), + spanDurationMS(networkRPC), + spanDurationMS(getConn), + spanDurationMS(openStream), + spanDurationMS(sendStart), + spanDurationMS(recvUntilExit), + spanDurationMS(vsockDial), + spanDurationMS(vsockUnixDial), + spanDurationMS(vsockWriteConnect), + spanDurationMS(vsockReadOK), + spanAttrInt(exec, "attempts"), + spanAttrInt(exec, "retryable_attempts"), + spanAttrInt(networkRPC, "attempts"), + spanAttrInt(networkRPC, "retryable_attempts"), + firstNonEmpty(spanAttrString(exec, "first_retryable_error_type"), spanAttrString(networkRPC, "first_retryable_error_type")), + firstNonEmpty(spanAttrString(exec, "last_retryable_error_type"), spanAttrString(networkRPC, "last_retryable_error_type")), + firstNonNegative(spanAttrInt(exec, "wait_elapsed_ms"), spanAttrInt(networkRPC, "wait_elapsed_ms")), + spanDurationsByName(desc, "guest.exec.open_stream"), + spanDurationsByName(desc, "guest.exec.recv_until_exit"), + spanDurationsByName(desc, "guest.reconfigure_network.rpc"), + spanDurationsByName(desc, "hypervisor.vsock.dial"), + spanDurationsByName(desc, "hypervisor.vsock.unix_dial"), + spanDurationsByName(desc, "hypervisor.vsock.write_connect"), + spanDurationsByName(desc, "hypervisor.vsock.read_ok"), + ) +} + +func descendantSpans(spans []sdktrace.ReadOnlySpan, root sdktrace.ReadOnlySpan) []sdktrace.ReadOnlySpan { + if root == nil { + return nil + } + descendantIDs := map[string]bool{root.SpanContext().SpanID().String(): true} + var out []sdktrace.ReadOnlySpan + for { + added := false + for _, span := range spans { + id := span.SpanContext().SpanID().String() + if descendantIDs[id] { + continue + } + parent := span.Parent().SpanID().String() + if descendantIDs[parent] { + descendantIDs[id] = true + out = append(out, span) + added = true + } + } + if !added { + return out + } + } +} + +func lastSpanNamed(spans []sdktrace.ReadOnlySpan, name string) sdktrace.ReadOnlySpan { + for i := len(spans) - 1; i >= 0; i-- { + if spans[i].Name() == name { + return spans[i] + } + } + return nil +} + +func spanDurationMS(span sdktrace.ReadOnlySpan) int64 { + if span == nil { + return -1 + } + return span.EndTime().Sub(span.StartTime()).Milliseconds() +} + +func spanAttrInt(span sdktrace.ReadOnlySpan, key string) int64 { + if span == nil { + return -1 + } + for _, attr := range span.Attributes() { + if string(attr.Key) != key { + continue + } + switch attr.Value.Type() { + case attribute.INT64: + return attr.Value.AsInt64() + case attribute.INT64SLICE: + return -1 + default: + return -1 + } + } + return -1 +} + +func spanAttrString(span sdktrace.ReadOnlySpan, key string) string { + if span == nil { + return "missing" + } + for _, attr := range span.Attributes() { + if string(attr.Key) == key { + return attr.Value.AsString() + } + } + return "" +} + +func spanDurationsByName(spans []sdktrace.ReadOnlySpan, name string) string { + var parts []string + for _, span := range spans { + if span.Name() != name { + continue + } + parts = append(parts, fmt.Sprintf("%d/%s", spanDurationMS(span), span.Status().Code.String())) + } + if len(parts) == 0 { + return "[]" + } + return "[" + strings.Join(parts, ",") + "]" +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if value != "" && value != "missing" { + return value + } + } + if len(values) == 0 { + return "" + } + return values[len(values)-1] +} + +func firstNonNegative(values ...int64) int64 { + for _, value := range values { + if value >= 0 { + return value + } + } + return -1 +} diff --git a/lib/instances/restore.go b/lib/instances/restore.go index 9f1a3034..28f88007 100644 --- a/lib/instances/restore.go +++ b/lib/instances/restore.go @@ -16,6 +16,8 @@ import ( "github.com/kernel/hypeman/lib/network" snapshotstore "github.com/kernel/hypeman/lib/snapshot" "go.opentelemetry.io/otel/attribute" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // RestoreInstance restores an instance from standby @@ -379,7 +381,7 @@ func (m *manager) restoreFromSnapshot( } func reconfigureGuestNetwork(ctx context.Context, stored *StoredMetadata, alloc *network.Allocation) error { - cmd, err := guestNetworkReconfigureCommand(alloc) + cfg, err := guestNetworkReconfigureConfig(alloc) if err != nil { return err } @@ -389,6 +391,30 @@ func reconfigureGuestNetwork(ctx context.Context, stored *StoredMetadata, alloc return fmt.Errorf("create vsock dialer: %w", err) } + err = guest.ReconfigureNetworkInInstance(ctx, dialer, guest.ReconfigureNetworkOptions{ + InterfaceName: "eth0", + MAC: cfg.mac, + IPv4: cfg.ip, + Prefix: uint32(cfg.prefix), + Gateway: cfg.gateway, + WaitForAgent: 120 * time.Second, + }) + if err != nil { + if status.Code(err) == codes.Unimplemented { + return reconfigureGuestNetworkWithExec(ctx, dialer, alloc) + } + return fmt.Errorf("reconfigure guest network: %w", err) + } + + return nil +} + +func reconfigureGuestNetworkWithExec(ctx context.Context, dialer hypervisor.VsockDialer, alloc *network.Allocation) error { + cmd, err := guestNetworkReconfigureCommand(alloc) + if err != nil { + return err + } + var stdout, stderr bytes.Buffer exit, err := guest.ExecIntoInstance(ctx, dialer, guest.ExecOptions{ Command: []string{"sh", "-c", cmd}, @@ -402,30 +428,44 @@ func reconfigureGuestNetwork(ctx context.Context, stored *StoredMetadata, alloc if exit.Code != 0 { return fmt.Errorf("network reconfiguration command failed (exit=%d, stdout=%q, stderr=%q)", exit.Code, strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String())) } - return nil } -func guestNetworkReconfigureCommand(alloc *network.Allocation) (string, error) { +type guestNetworkConfig struct { + ip string + mac string + gateway string + prefix int +} + +func guestNetworkReconfigureConfig(alloc *network.Allocation) (*guestNetworkConfig, error) { if alloc == nil { - return "", fmt.Errorf("missing network allocation") + return nil, fmt.Errorf("missing network allocation") } ip := strings.TrimSpace(alloc.IP) if ip == "" { - return "", fmt.Errorf("missing network allocation IP") + return nil, fmt.Errorf("missing network allocation IP") } mac := strings.ToLower(strings.TrimSpace(alloc.MAC)) if mac == "" { - return "", fmt.Errorf("missing network allocation MAC") + return nil, fmt.Errorf("missing network allocation MAC") } if _, err := net.ParseMAC(mac); err != nil { - return "", fmt.Errorf("invalid network allocation MAC %q: %w", alloc.MAC, err) + return nil, fmt.Errorf("invalid network allocation MAC %q: %w", alloc.MAC, err) } gateway := strings.TrimSpace(alloc.Gateway) if gateway == "" { - return "", fmt.Errorf("missing network allocation gateway") + return nil, fmt.Errorf("missing network allocation gateway") } prefix, err := netmaskToPrefix(alloc.Netmask) + if err != nil { + return nil, err + } + return &guestNetworkConfig{ip: ip, mac: mac, gateway: gateway, prefix: prefix}, nil +} + +func guestNetworkReconfigureCommand(alloc *network.Allocation) (string, error) { + cfg, err := guestNetworkReconfigureConfig(alloc) if err != nil { return "", err } @@ -445,7 +485,7 @@ func guestNetworkReconfigureCommand(alloc *network.Allocation) (string, error) { "ip route replace default via %s dev eth0 && "+ // Drop snapshotted ARP/neighbor entries so peers are rediscovered. "(ip neigh flush dev eth0 || true)", - mac, ip, prefix, gateway, + cfg.mac, cfg.ip, cfg.prefix, cfg.gateway, ), nil } diff --git a/lib/instances/restore_egress_test.go b/lib/instances/restore_egress_test.go index f65b0eb1..a8ce2a74 100644 --- a/lib/instances/restore_egress_test.go +++ b/lib/instances/restore_egress_test.go @@ -30,7 +30,25 @@ func TestNetworkConfigFromAllocation_PreservesDNS(t *testing.T) { assert.Equal(t, alloc.TAPDevice, cfg.TAPDevice) } -func TestGuestNetworkReconfigureCommand_AppliesAllocatedMAC(t *testing.T) { +func TestGuestNetworkReconfigureConfig_AppliesAllocatedMAC(t *testing.T) { + t.Parallel() + + alloc := &network.Allocation{ + IP: "10.102.146.62", + MAC: "02:00:00:85:17:c8", + Gateway: "10.102.0.1", + Netmask: "255.255.0.0", + } + + cfg, err := guestNetworkReconfigureConfig(alloc) + require.NoError(t, err) + assert.Equal(t, "10.102.146.62", cfg.ip) + assert.Equal(t, "02:00:00:85:17:c8", cfg.mac) + assert.Equal(t, "10.102.0.1", cfg.gateway) + assert.Equal(t, 16, cfg.prefix) +} + +func TestGuestNetworkReconfigureCommand_FallbackPreservesShellBehavior(t *testing.T) { t.Parallel() alloc := &network.Allocation{ @@ -42,18 +60,15 @@ func TestGuestNetworkReconfigureCommand_AppliesAllocatedMAC(t *testing.T) { cmd, err := guestNetworkReconfigureCommand(alloc) require.NoError(t, err) - assert.Contains(t, cmd, "ip link set dev eth0 down") assert.Contains(t, cmd, "ip link set dev eth0 address 02:00:00:85:17:c8") assert.Contains(t, cmd, "ip addr add 10.102.146.62/16 dev eth0") - assert.Contains(t, cmd, "ip route replace default via 10.102.0.1 dev eth0") assert.Contains(t, cmd, "(ip neigh flush dev eth0 || true)") - assert.NotContains(t, cmd, "cat /sys/class/net/eth0/address") } -func TestGuestNetworkReconfigureCommand_RequiresAllocatedMAC(t *testing.T) { +func TestGuestNetworkReconfigureConfig_RequiresAllocatedMAC(t *testing.T) { t.Parallel() - _, err := guestNetworkReconfigureCommand(&network.Allocation{ + _, err := guestNetworkReconfigureConfig(&network.Allocation{ IP: "10.102.146.62", Gateway: "10.102.0.1", Netmask: "255.255.0.0", diff --git a/lib/system/guest_agent/network.go b/lib/system/guest_agent/network.go new file mode 100644 index 00000000..66b71bad --- /dev/null +++ b/lib/system/guest_agent/network.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "fmt" + "net" + + pb "github.com/kernel/hypeman/lib/guest" + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" +) + +func (s *guestServer) ReconfigureNetwork(_ context.Context, req *pb.ReconfigureNetworkRequest) (*pb.ReconfigureNetworkResponse, error) { + iface := req.InterfaceName + if iface == "" { + iface = "eth0" + } + + link, err := netlink.LinkByName(iface) + if err != nil { + return nil, fmt.Errorf("find interface %s: %w", iface, err) + } + + mac, err := net.ParseMAC(req.Mac) + if err != nil { + return nil, fmt.Errorf("parse mac %q: %w", req.Mac, err) + } + ip := net.ParseIP(req.Ipv4).To4() + if ip == nil { + return nil, fmt.Errorf("parse ipv4 %q", req.Ipv4) + } + if req.Prefix > 32 { + return nil, fmt.Errorf("invalid ipv4 prefix %d", req.Prefix) + } + gateway := net.ParseIP(req.Gateway).To4() + if gateway == nil { + return nil, fmt.Errorf("parse gateway %q", req.Gateway) + } + + if err := netlink.LinkSetDown(link); err != nil { + return nil, fmt.Errorf("set %s down: %w", iface, err) + } + if err := netlink.LinkSetHardwareAddr(link, mac); err != nil { + return nil, fmt.Errorf("set %s mac: %w", iface, err) + } + if err := flushIPv4Addrs(link); err != nil { + return nil, err + } + addr := &netlink.Addr{IPNet: &net.IPNet{IP: ip, Mask: net.CIDRMask(int(req.Prefix), 32)}} + if err := netlink.AddrAdd(link, addr); err != nil { + return nil, fmt.Errorf("add ipv4 address: %w", err) + } + if err := netlink.LinkSetUp(link); err != nil { + return nil, fmt.Errorf("set %s up: %w", iface, err) + } + if err := netlink.RouteReplace(&netlink.Route{ + LinkIndex: link.Attrs().Index, + Gw: gateway, + }); err != nil { + return nil, fmt.Errorf("replace default route: %w", err) + } + _ = flushNeighbors(link) + + return &pb.ReconfigureNetworkResponse{}, nil +} + +func flushIPv4Addrs(link netlink.Link) error { + addrs, err := netlink.AddrList(link, unix.AF_INET) + if err != nil { + return fmt.Errorf("list ipv4 addresses: %w", err) + } + for _, addr := range addrs { + if err := netlink.AddrDel(link, &addr); err != nil { + return fmt.Errorf("delete ipv4 address %s: %w", addr.String(), err) + } + } + return nil +} + +func flushNeighbors(link netlink.Link) error { + neighbors, err := netlink.NeighList(link.Attrs().Index, unix.AF_INET) + if err != nil { + return fmt.Errorf("list neighbors: %w", err) + } + for _, neighbor := range neighbors { + if err := netlink.NeighDel(&neighbor); err != nil { + return fmt.Errorf("delete neighbor %s: %w", neighbor.String(), err) + } + } + return nil +}