From 79ca8969d90cf2dc35e9cf0c41cbda4a257acc28 Mon Sep 17 00:00:00 2001 From: huangyichen Date: Tue, 26 Dec 2023 09:46:24 +0800 Subject: [PATCH 01/63] 1. set query params 'snapshot' of API(/v2/vs) to dump the running service into cache file. 2. resume running service with snapshot cache file by launch params 'init-mode=local' 3. new API(/v2/vs/${SERVICE-ID}/health) for dpvs-healthcheck --- .../cmd/device/put_device_name_addr.go | 7 + .../device/put_device_name_netlink_addr.go | 51 +-- .../cmd/dpvs-agent-server/api_init.go | 85 ++++- .../cmd/dpvs-agent-server/local_init.go | 121 +++++++ .../dpvs-agent/cmd/ipvs/delete_vs_vip_port.go | 3 + tools/dpvs-agent/cmd/ipvs/get_vs.go | 46 ++- tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go | 8 +- .../cmd/ipvs/post_vs_vip_port_rs.go | 2 + tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go | 3 +- .../dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go | 12 +- .../cmd/ipvs/put_vs_vip_port_rs_health.go | 111 +++++++ tools/dpvs-agent/dpvs-agent-api.yaml | 109 +++++-- tools/dpvs-agent/models/dpvs_node_spec.go | 150 +++++++++ .../models/node_service_snapshot.go | 150 +++++++++ .../models/virtual_server_spec_expand.go | 3 + tools/dpvs-agent/models/vs_announce_port.go | 53 +++ tools/dpvs-agent/pkg/ipc/types/snapshot.go | 130 ++++++++ tools/dpvs-agent/pkg/settings/settings.go | 43 +++ tools/dpvs-agent/pkg/settings/util.go | 18 ++ tools/dpvs-agent/restapi/embedded_spec.go | 306 +++++++++++++++++- .../device/put_device_name_addr_parameters.go | 39 ++- .../device/put_device_name_addr_urlbuilder.go | 11 +- ...put_device_name_netlink_addr_parameters.go | 50 ++- ...put_device_name_netlink_addr_urlbuilder.go | 16 + .../restapi/operations/dpvs_agent_api.go | 12 + .../virtualserver/get_vs_parameters.go | 39 ++- .../virtualserver/get_vs_urlbuilder.go | 11 +- .../get_vs_vip_port_laddr_parameters.go | 39 ++- .../get_vs_vip_port_laddr_urlbuilder.go | 11 +- .../get_vs_vip_port_parameters.go | 39 ++- .../get_vs_vip_port_rs_parameters.go | 39 ++- .../get_vs_vip_port_rs_urlbuilder.go | 11 +- .../get_vs_vip_port_urlbuilder.go | 11 +- .../post_vs_vip_port_rs_parameters.go | 50 ++- .../post_vs_vip_port_rs_urlbuilder.go | 16 + .../put_vs_vip_port_laddr_parameters.go | 50 ++- .../put_vs_vip_port_laddr_urlbuilder.go | 16 + .../put_vs_vip_port_parameters.go | 50 ++- .../put_vs_vip_port_rs_health.go | 56 ++++ .../put_vs_vip_port_rs_health_parameters.go | 134 ++++++++ .../put_vs_vip_port_rs_health_responses.go | 233 +++++++++++++ .../put_vs_vip_port_rs_health_urlbuilder.go | 110 +++++++ .../put_vs_vip_port_rs_parameters.go | 50 +-- .../put_vs_vip_port_rs_urlbuilder.go | 16 - .../put_vs_vip_port_urlbuilder.go | 16 + 45 files changed, 2365 insertions(+), 171 deletions(-) create mode 100644 tools/dpvs-agent/cmd/dpvs-agent-server/local_init.go create mode 100644 tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go create mode 100644 tools/dpvs-agent/models/dpvs_node_spec.go create mode 100644 tools/dpvs-agent/models/node_service_snapshot.go create mode 100644 tools/dpvs-agent/models/vs_announce_port.go create mode 100644 tools/dpvs-agent/pkg/ipc/types/snapshot.go create mode 100644 tools/dpvs-agent/pkg/settings/settings.go create mode 100644 tools/dpvs-agent/pkg/settings/util.go create mode 100644 tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health.go create mode 100644 tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_parameters.go create mode 100644 tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_responses.go create mode 100644 tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_urlbuilder.go diff --git a/tools/dpvs-agent/cmd/device/put_device_name_addr.go b/tools/dpvs-agent/cmd/device/put_device_name_addr.go index 275e33100..4dff0e3eb 100644 --- a/tools/dpvs-agent/cmd/device/put_device_name_addr.go +++ b/tools/dpvs-agent/cmd/device/put_device_name_addr.go @@ -17,6 +17,7 @@ package device import ( "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" + "github.com/dpvs-agent/pkg/settings" apiDevice "github.com/dpvs-agent/restapi/operations/device" @@ -52,6 +53,12 @@ func (h *putDeviceAddr) Handle(params apiDevice.PutDeviceNameAddrParams) middlew if params.Sapool != nil && *params.Sapool { addr.SetFlags("sapool") } + + if params.Snapshot != nil && *params.Snapshot { + AnnouncePort := settings.ShareSnapshot().NodeSpec.AnnouncePort + AnnouncePort.Dpvs = params.Name + } + // addr.SetValidLft(prarms.Spec.ValidLft) // addr.SetPreferedLft(prarms.Spec.ValidLft) diff --git a/tools/dpvs-agent/cmd/device/put_device_name_netlink_addr.go b/tools/dpvs-agent/cmd/device/put_device_name_netlink_addr.go index 1b093b431..c03495990 100644 --- a/tools/dpvs-agent/cmd/device/put_device_name_netlink_addr.go +++ b/tools/dpvs-agent/cmd/device/put_device_name_netlink_addr.go @@ -15,6 +15,7 @@ package device import ( + "errors" "fmt" "net" "strings" @@ -22,6 +23,7 @@ import ( "github.com/vishvananda/netlink" "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/settings" apiDevice "github.com/dpvs-agent/restapi/operations/device" "github.com/go-openapi/runtime/middleware" @@ -44,43 +46,54 @@ func NewPutDeviceNetlinkAddr(cp *pool.ConnPool, parentLogger hclog.Logger) *putD // ip addr add 10.0.0.1/32 dev eth0 func (h *putDeviceNetlinkAddr) Handle(params apiDevice.PutDeviceNameNetlinkAddrParams) middleware.Responder { // h.logger.Info("/v2/device/", params.Name, "/netlink/addr ", params.Spec.Addr) + if err := NetlinkAddrAdd(params.Spec.Addr, params.Name, h.logger); err != nil { + return apiDevice.NewPutDeviceNameNetlinkAddrInternalServerError() + } + if params.Snapshot != nil && *params.Snapshot { + AnnouncePort := settings.ShareSnapshot().NodeSpec.AnnouncePort + AnnouncePort.Switch = params.Name + } + return apiDevice.NewPutDeviceNameNetlinkAddrOK() +} + +func NetlinkAddrAdd(addr, device string, logger hclog.Logger) error { var cidr string - if strings.Count(params.Spec.Addr, "/") == 0 { - ip := net.ParseIP(params.Spec.Addr) + if strings.Count(addr, "/") == 0 { + ip := net.ParseIP(addr) if ip == nil { - h.logger.Info("Parse IP failed.", "Addr", params.Spec.Addr) - return apiDevice.NewPutDeviceNameNetlinkAddrInternalServerError() + logger.Info("Parse IP failed.", "Addr", addr) + return errors.New("Parse IP Failed.") } if ip.To4() != nil { - cidr = params.Spec.Addr + "/32" + cidr = addr + "/32" } else { - cidr = params.Spec.Addr + "/128" + cidr = addr + "/128" } } else { - cidr = params.Spec.Addr + cidr = addr } ip, ipnet, err := net.ParseCIDR(cidr) if err != nil { - h.logger.Error("Parse CIDR failed.", "cidr", cidr, "Error", err.Error()) - return apiDevice.NewPutDeviceNameNetlinkAddrInternalServerError() + logger.Error("Parse CIDR failed.", "cidr", cidr, "Error", err.Error()) + return err } ipnet.IP = ip - addr := &netlink.Addr{IPNet: ipnet} + netlinkAddr := &netlink.Addr{IPNet: ipnet} - link, err := netlink.LinkByName(params.Name) + link, err := netlink.LinkByName(device) if err != nil { - h.logger.Error("netlink.LinkByName() failed.", "Device Name", params.Name, "Error", err.Error()) - return apiDevice.NewPutDeviceNameNetlinkAddrInternalServerError() + logger.Error("netlink.LinkByName() failed.", "device", device, "Error", err.Error()) + return err } - if err := netlink.AddrAdd(link, addr); err != nil { - h.logger.Error("netlink.AddrAdd() failed.", "Error", err.Error()) - return apiDevice.NewPutDeviceNameNetlinkAddrInternalServerError() + if err := netlink.AddrAdd(link, netlinkAddr); err != nil { + logger.Error("netlink.AddrAdd() failed.", "Error", err.Error()) + return err } - cmd := fmt.Sprintf("ip addr add %s dev %s", cidr, params.Name) - h.logger.Info("Device add Addr success.", "cmd", cmd) - return apiDevice.NewPutDeviceNameNetlinkAddrOK() + cmd := fmt.Sprintf("ip addr add %s dev %s", cidr, device) + logger.Info("Device add Addr success.", "cmd", cmd) + return nil } diff --git a/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go b/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go index bc269e4ff..7912acbce 100644 --- a/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go +++ b/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go @@ -17,8 +17,10 @@ package main import ( "context" "errors" + "fmt" "net" "os" + "path/filepath" "strings" "time" @@ -28,6 +30,7 @@ import ( "github.com/dpvs-agent/cmd/device" "github.com/dpvs-agent/cmd/ipvs" "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/settings" "github.com/dpvs-agent/restapi" "github.com/dpvs-agent/restapi/operations" ) @@ -37,7 +40,9 @@ var ( ) type DpvsAgentServer struct { + InitMode string `long:"init-mode" description:"load service from network or local config file. the options is [network|local]" default:"network"` LogDir string `long:"log-dir" description:"default log dir is /var/log/ And log name dpvs-agent.log" default:"/var/log/"` + CacheFile string `long:"cache-file" description:"a file path which used to dump the running dpvs active virtual service. we can load it while init by *local* mode and resume dpvs enviroment. if the file path is not specified, there is named with 'dpvs.cache' and store in 'conf.d' which is a subdir of 'LogDir' point to." default:""` IpcSocketPath string `long:"ipc-sockopt-path" description:"default ipc socket path /var/run/dpvs.ipc" default:"/var/run/dpvs.ipc"` restapi.Server } @@ -65,6 +70,64 @@ func unixDialer(ctx context.Context) (net.Conn, error) { return nil, errors.New("unknown error") } +func validFile(fileName string) error { + filePath := fileName[:strings.LastIndex(fileName, "/")] + pathInfo, err := os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + err = os.MkdirAll(filePath, os.ModePerm) + if err != nil { + return err + } + return nil + } + return err + } + + if !pathInfo.IsDir() { + return errors.New(fmt.Sprintf("%s is file", pathInfo.Name())) + } + + fileInfo, err := os.Stat(fileName) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + if fileInfo.IsDir() { + return errors.New(fmt.Sprintf("%s is dir", fileInfo.Name())) + } + + return nil +} + +func getFilePath(baseDir, defaultSubdir, targetFile, defaultName string) string { + if len(targetFile) == 0 || strings.EqualFold(targetFile, "/") { + return filepath.Join(baseDir, defaultSubdir, defaultName) + } else { + if strings.HasPrefix(targetFile, "/") { + if strings.HasSuffix(targetFile, "/") { + return filepath.Join(targetFile, defaultName) + } else { + return targetFile + } + } else { + if strings.Count(targetFile, "/") == 0 { + return filepath.Join(baseDir, defaultSubdir, targetFile) + } else { + if strings.HasSuffix(targetFile, "/") { + return filepath.Join(baseDir, targetFile, defaultName) + } else { + return filepath.Join(baseDir, targetFile) + } + } + } + } + return targetFile +} + func (agent *DpvsAgentServer) instantiateAPI(restAPI *operations.DpvsAgentAPI) { if strings.HasSuffix(agent.IpcSocketPath, ".ipc") { s, err := os.Stat(agent.IpcSocketPath) @@ -93,12 +156,17 @@ func (agent *DpvsAgentServer) instantiateAPI(restAPI *operations.DpvsAgentAPI) { } } - sep := "/" - if strings.HasSuffix(logDir, "/") { - sep = "" + cacheFile := getFilePath(logDir, "conf.d", agent.CacheFile, "dpvs.cache") + if err := validFile(cacheFile); err != nil { + panic(err) } + appConf := settings.ShareAppConfig() + appConf.CacheFile = cacheFile - logFile := strings.Join([]string{logDir, "dpvs-agent.log"}, sep) + logFile := getFilePath(logDir, ".", "", "dpvs-agent.log") + if err := validFile(logFile); err != nil { + panic(err) + } // logOpt := &hclog.LoggerOptions{Name: logFile} var logOpt *hclog.LoggerOptions logFileNamePattern := strings.Join([]string{logFile, "%Y%m%d%H%M"}, "-") @@ -109,7 +177,6 @@ func (agent *DpvsAgentServer) instantiateAPI(restAPI *operations.DpvsAgentAPI) { rotatelogs.WithLinkName(logFile), rotatelogs.WithRotationTime(logRotationInterval), ) - // f, err := os.Create(logFile) if err == nil { logOpt = &hclog.LoggerOptions{Name: logFile, Output: logF} } else { @@ -136,6 +203,7 @@ func (agent *DpvsAgentServer) instantiateAPI(restAPI *operations.DpvsAgentAPI) { restAPI.VirtualserverPutVsVipPortHandler = ipvs.NewPutVsItem(cp, logger) restAPI.VirtualserverPutVsVipPortLaddrHandler = ipvs.NewPutVsLaddr(cp, logger) restAPI.VirtualserverPutVsVipPortRsHandler = ipvs.NewPutVsRs(cp, logger) + restAPI.VirtualserverPutVsVipPortRsHealthHandler = ipvs.NewPutVsRsHealth(cp, logger) restAPI.VirtualserverPutVsVipPortDenyHandler = ipvs.NewPutVsDeny(cp, logger) restAPI.VirtualserverPutVsVipPortAllowHandler = ipvs.NewPutVsAllow(cp, logger) @@ -161,6 +229,13 @@ func (agent *DpvsAgentServer) instantiateAPI(restAPI *operations.DpvsAgentAPI) { restAPI.DeviceDeleteDeviceNameRouteHandler = device.NewDelDeviceRoute(cp, logger) restAPI.DeviceDeleteDeviceNameVlanHandler = device.NewDelDeviceVlan(cp, logger) restAPI.DeviceDeleteDeviceNameNetlinkAddrHandler = device.NewDelDeviceNetlinkAddr(cp, logger) + + switch strings.ToLower(agent.InitMode) { + case "network": + case "local": + agent.Host = "127.0.0.1" + agent.LocalLoad(cp, logger) + } } func (agent *DpvsAgentServer) InstantiateServer(api *operations.DpvsAgentAPI) *restapi.Server { diff --git a/tools/dpvs-agent/cmd/dpvs-agent-server/local_init.go b/tools/dpvs-agent/cmd/dpvs-agent-server/local_init.go new file mode 100644 index 000000000..f5a8a6968 --- /dev/null +++ b/tools/dpvs-agent/cmd/dpvs-agent-server/local_init.go @@ -0,0 +1,121 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "strings" + + "github.com/dpvs-agent/cmd/device" + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/ipc/types" + "github.com/dpvs-agent/pkg/settings" + + "github.com/hashicorp/go-hclog" +) + +func (agent *DpvsAgentServer) LocalLoad(cp *pool.ConnPool, parentLogger hclog.Logger) error { + var errs []error + logger := hclog.Default().Named("LoadConfigFile") + if parentLogger != nil { + logger = parentLogger.Named("LoadConfigFile") + } + + snapshot := settings.ShareSnapshot() + if err := snapshot.LoadFrom(settings.LocalConfigFile(), logger); err != nil { + return err + } + + announcePort := snapshot.NodeSpec.AnnouncePort + laddrs := snapshot.NodeSpec.Laddrs + + for _, service := range snapshot.Services { + // 1> ipvsadm -A vip:port -s wrr + vs := types.NewVirtualServerSpec() + vs.SetAddr(service.Addr) + vs.SetPort(service.Port) + vs.SetProto(service.Proto) + vs.SetFwmark(service.Fwmark) + vs.SetConnTimeout(service.ConnTimeout) + vs.SetBps(service.Bps) + vs.SetLimitProportion(service.LimitProportion) + vs.SetTimeout(service.Timeout) + vs.SetSchedName(service.SchedName) + flags := strings.ToLower(service.Flags) + if strings.Index(flags, "expirequiescent") != -1 { + vs.SetFlagsExpireQuiescent() + } + if strings.Index(flags, "synproxy") != -1 { + vs.SetFlagsSynProxy() + } + if strings.Index(flags, "conhashbysrcip") != -1 { + vs.SetFlagsHashSrcIP() + } + if strings.Index(flags, "conhashbyquicid") != -1 { + vs.SetFlagsHashQuicID() + } + vs.Add(cp, logger) + // 2> dpip addr add ${vip} dev ${device} + svcAddr := types.NewInetAddrDetail() + svcAddr.SetAddr(service.Addr) + svcAddr.SetIfName(announcePort.Dpvs) + svcAddr.Add(cp, logger) + + // 3> ipvsadm -at ${VIPPORT} -r ${RS:PORT} -w ${WEIGHT} -b + rsFront := types.NewRealServerFront() + if err := rsFront.ParseVipPortProto(vs.ID()); err != nil { + errs = append(errs, err) + } + rss := make([]*types.RealServerSpec, len(service.RSs.Items)) + for i, rs := range service.RSs.Items { + var fwdmode types.DpvsFwdMode + fwdmode.FromString(rs.Spec.Mode) + rss[i] = types.NewRealServerSpec() + rss[i].SetPort(rs.Spec.Port) + rss[i].SetWeight(uint32(rs.Spec.Weight)) + rss[i].SetProto(uint16(service.Proto)) + rss[i].SetAddr(rs.Spec.IP) + rss[i].SetFwdMode(fwdmode) + } + + rsFront.Update(rss, cp, logger) + // 4> bind laddr with vs (ipvsadm --add-laddr -z ${LADDR} -t ${VIPPORT} -F ${device}) + laddr := types.NewLocalAddrFront() + if err := laddr.ParseVipPortProto(vs.ID()); err != nil { + } + lds := make([]*types.LocalAddrDetail, len(laddrs.Items)) + for i, lip := range laddrs.Items { + lds[i] = types.NewLocalAddrDetail() + lds[i].SetAddr(lip.Addr) + lds[i].SetIfName(lip.Device) + } + laddr.Add(lds, cp, logger) + // 5> ip addr add ${VIP} dev ${KNIDEVICE(lo?)} + if err := device.NetlinkAddrAdd(service.Addr, announcePort.Switch, logger); err != nil { + logger.Error("add addr", service.Addr, "onto device failed") + errs = append(errs, err) + } + } + // 6> dpip addr add ${LADDR} dev ${device} + for _, lip := range laddrs.Items { + lipAddr := types.NewInetAddrDetail() + lipAddr.SetAddr(lip.Addr) + lipAddr.SetIfName(lip.Device) + lipAddr.SetFlags("sapool") + resultCode := lipAddr.Add(cp, logger) + logger.Info("Add addr to device done.", "Device", lip.Device, "Addr", lip.Addr, "result", resultCode.String()) + } + + return settings.MergedError(errs) +} diff --git a/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port.go b/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port.go index 280827cee..03a0f7a6c 100644 --- a/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port.go +++ b/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port.go @@ -17,6 +17,7 @@ package ipvs import ( "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" + "github.com/dpvs-agent/pkg/settings" apiVs "github.com/dpvs-agent/restapi/operations/virtualserver" @@ -47,9 +48,11 @@ func (h *delVsItem) Handle(params apiVs.DeleteVsVipPortParams) middleware.Respon result := vs.Del(h.connPool, h.logger) switch result { case types.EDPVS_OK: + settings.ShareSnapshot().ServiceDel(params.VipPort) h.logger.Info("Del virtual server success.", "VipPort", params.VipPort) return apiVs.NewDeleteVsVipPortOK() case types.EDPVS_NOTEXIST: + settings.ShareSnapshot().ServiceDel(params.VipPort) h.logger.Warn("Del a not exist virtual server done.", "VipPort", params.VipPort, "result", result.String()) return apiVs.NewDeleteVsVipPortNotFound() default: diff --git a/tools/dpvs-agent/cmd/ipvs/get_vs.go b/tools/dpvs-agent/cmd/ipvs/get_vs.go index 700931f75..d9cd5b798 100644 --- a/tools/dpvs-agent/cmd/ipvs/get_vs.go +++ b/tools/dpvs-agent/cmd/ipvs/get_vs.go @@ -18,6 +18,7 @@ import ( "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" + "github.com/dpvs-agent/pkg/settings" apiVs "github.com/dpvs-agent/restapi/operations/virtualserver" @@ -47,10 +48,10 @@ func (h *getVs) Handle(params apiVs.GetVsParams) middleware.Responder { return apiVs.NewGetVsOK() } + shareSnapshot := settings.ShareSnapshot() + h.logger.Info("Get all virtual server done.", "vss", vss) - vsModels := new(models.VirtualServerList) - vsModels.Items = make([]*models.VirtualServerSpecExpand, len(vss)) - for i, vs := range vss { + for _, vs := range vss { front := types.NewRealServerFront() err := front.ParseVipPortProto(vs.ID()) @@ -68,18 +69,45 @@ func (h *getVs) Handle(params apiVs.GetVsParams) middleware.Responder { h.logger.Info("Get real server list of virtual server success.", "ID", vs.ID(), "rss", rss) - vsModels.Items[i] = vs.GetModel() - vsStats := (*types.ServerStats)(vsModels.Items[i].Stats) - vsModels.Items[i].RSs = new(models.RealServerExpandList) - vsModels.Items[i].RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) + vsModel := vs.GetModel() + vsStats := (*types.ServerStats)(vsModel.Stats) + vsModel.RSs = new(models.RealServerExpandList) + vsModel.RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) for j, rs := range rss { rsModel := rs.GetModel() rsStats := (*types.ServerStats)(rsModel.Stats) - vsModels.Items[i].RSs.Items[j] = rsModel + vsModel.RSs.Items[j] = rsModel vsStats.Increase(rsStats) } + + if shareSnapshot.NodeSpec.Laddrs == nil { + laddr := types.NewLocalAddrFront() + if err := laddr.ParseVipPortProto(vs.ID()); err != nil { + // FIXME: Invalid + return apiVs.NewGetVsOK() + } + + laddrs, err := laddr.Get(h.connPool, h.logger) + if err != nil { + // FIXME: Invalid + return apiVs.NewGetVsOK() + } + + shareSnapshot.NodeSpec.Laddrs = new(models.LocalAddressExpandList) + laddrModels := shareSnapshot.NodeSpec.Laddrs + laddrModels.Items = make([]*models.LocalAddressSpecExpand, len(laddrs)) + for k, lip := range laddrs { + laddrModels.Items[k] = lip.GetModel() + } + } + + shareSnapshot.ServiceUpsert(vsModel) + } + + if params.Snapshot != nil && *params.Snapshot { + shareSnapshot.DumpTo(settings.LocalConfigFile(), h.logger) } - return apiVs.NewGetVsOK().WithPayload(vsModels) + return apiVs.NewGetVsOK().WithPayload(shareSnapshot.GetModels(h.logger)) } diff --git a/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go b/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go index c9f907876..91c7bc60c 100644 --- a/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go +++ b/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go @@ -18,6 +18,7 @@ import ( "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" + "github.com/dpvs-agent/pkg/settings" apiVs "github.com/dpvs-agent/restapi/operations/virtualserver" @@ -55,6 +56,8 @@ func (h *getVsVipPort) Handle(params apiVs.GetVsVipPortParams) middleware.Respon return apiVs.NewGetVsVipPortNotFound() } + shareSnapshot := settings.ShareSnapshot() + vsModels := new(models.VirtualServerList) vsModels.Items = make([]*models.VirtualServerSpecExpand, len(vss)) @@ -76,7 +79,10 @@ func (h *getVsVipPort) Handle(params apiVs.GetVsVipPortParams) middleware.Respon h.logger.Info("Get real server list of virtual server success.", "ID", vs.ID(), "rss", rss) - vsModels.Items[i] = vs.GetModel() + vsModel := vs.GetModel() + shareSnapshot.ServiceUpsert(vsModel) + // vsModel.Version = shareSnapshot.ServiceVersion(vs.ID()) + vsModels.Items[i] = vsModel vsStats := (*types.ServerStats)(vsModels.Items[i].Stats) vsModels.Items[i].RSs = new(models.RealServerExpandList) vsModels.Items[i].RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) diff --git a/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go index a7c9378a1..aa4c98506 100644 --- a/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go @@ -18,6 +18,7 @@ import ( // "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" + "github.com/dpvs-agent/pkg/settings" apiVs "github.com/dpvs-agent/restapi/operations/virtualserver" @@ -63,6 +64,7 @@ func (h *postVsRs) Handle(params apiVs.PostVsVipPortRsParams) middleware.Respond result := front.Update(rss, h.connPool, h.logger) switch result { case types.EDPVS_EXIST, types.EDPVS_OK: + settings.ShareSnapshot().ServiceVersionUpdate(params.VipPort, h.logger) h.logger.Info("Set real server to virtual server success.", "VipPort", params.VipPort, "rss", rss, "result", result.String()) return apiVs.NewPostVsVipPortRsOK() default: diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go index e96b4c697..0ee30e708 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go @@ -17,9 +17,9 @@ package ipvs import ( "strings" - // "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" + "github.com/dpvs-agent/pkg/settings" "golang.org/x/sys/unix" apiVs "github.com/dpvs-agent/restapi/operations/virtualserver" @@ -90,6 +90,7 @@ func (h *putVsItem) Handle(params apiVs.PutVsVipPortParams) middleware.Responder switch result { case types.EDPVS_OK: // return 201 + settings.ShareSnapshot().ServiceAdd(vs) h.logger.Info("Created new virtual server success.", "VipPort", params.VipPort) return apiVs.NewPutVsVipPortCreated() case types.EDPVS_EXIST: diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go index acfeb572f..2c596ab27 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go @@ -62,12 +62,8 @@ func (h *putVsRs) Handle(params apiVs.PutVsVipPortRsParams) middleware.Responder } } - healthCheck := false - if params.Healthcheck != nil { - healthCheck = *params.Healthcheck - } - - result := front.Edit(healthCheck, rss, h.connPool, h.logger) + existOnly := false + result := front.Edit(existOnly, rss, h.connPool, h.logger) // h.logger.Info("Set real server sets done.", "VipPort", params.VipPort, "rss", rss, "result", result.String()) switch result { @@ -75,10 +71,6 @@ func (h *putVsRs) Handle(params apiVs.PutVsVipPortRsParams) middleware.Responder h.logger.Info("Set real server sets success.", "VipPort", params.VipPort, "rss", rss, "result", result.String()) return apiVs.NewPutVsVipPortRsOK() case types.EDPVS_NOTEXIST: - if healthCheck { - h.logger.Error("Edit not exist real server.", "VipPort", params.VipPort, "rss", rss, "result", result.String()) - return apiVs.NewPutVsVipPortRsInvalidFrontend() - } h.logger.Error("Unreachable branch") default: h.logger.Error("Set real server sets failed.", "VipPort", params.VipPort, "rss", rss, "result", result.String()) diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go new file mode 100644 index 000000000..31d56449f --- /dev/null +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go @@ -0,0 +1,111 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipvs + +import ( + "strings" + + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/ipc/types" + "github.com/dpvs-agent/pkg/settings" + + "github.com/dpvs-agent/models" + apiVs "github.com/dpvs-agent/restapi/operations/virtualserver" + + "github.com/go-openapi/runtime/middleware" + "github.com/hashicorp/go-hclog" +) + +type putVsRsHealth struct { + connPool *pool.ConnPool + logger hclog.Logger +} + +func NewPutVsRsHealth(cp *pool.ConnPool, parentLogger hclog.Logger) *putVsRsHealth { + logger := hclog.Default() + if parentLogger != nil { + logger = parentLogger.Named("PutVsVipPortRsHealth") + } + return &putVsRsHealth{connPool: cp, logger: logger} +} + +func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middleware.Responder { + front := types.NewRealServerFront() + if err := front.ParseVipPortProto(params.VipPort); err != nil { + h.logger.Error("Convert to virtual server failed", "VipPort", params.VipPort, "Error", err.Error()) + return apiVs.NewPutVsVipPortRsHealthInvalidFrontend() + } + + // get active backends + active, err := front.Get(h.connPool, h.logger) + if err != nil { + return apiVs.NewPutVsVipPortRsHealthInvalidBackend() + } + + shareSnapshot := settings.ShareSnapshot() + version := shareSnapshot.ServiceVersion(params.VipPort) + + activeRSs := make(map[string]*types.RealServerSpec) + for _, rs := range active { + activeRSs[rs.ID()] = rs + } + + rssModels := new(models.RealServerExpandList) + rssModels.Items = make([]*models.RealServerSpecExpand, len(active)) + validRSs := make([]*types.RealServerSpec, 0) + if params.Rss != nil { + for i, rs := range params.Rss.Items { + var fwdmode types.DpvsFwdMode + fwdmode.FromString(rs.Mode) + newRs := types.NewRealServerSpec() + newRs.SetAf(front.GetAf()) + newRs.SetAddr(rs.IP) + newRs.SetPort(rs.Port) + newRs.SetProto(front.GetProto()) + newRs.SetWeight(uint32(rs.Weight)) + newRs.SetFwdMode(fwdmode) + newRs.SetInhibited(rs.Inhibited) + newRs.SetOverloaded(rs.Overloaded) + + if activeRs, existed := activeRSs[newRs.ID()]; existed { + rssModels.Items[i] = activeRs.GetModel() + validRSs = append(validRSs, newRs) + } + } + } + + if !strings.EqualFold(params.Version, version) { + h.logger.Info("The service", "VipPort", params.VipPort, "version expired. The newest version", version) + return apiVs.NewPutVsVipPortRsHealthUnexpected().WithPayload(rssModels) + } + + existOnly := true + result := front.Edit(existOnly, validRSs, h.connPool, h.logger) + switch result { + case types.EDPVS_EXIST, types.EDPVS_OK: + h.logger.Info("Set real server sets success.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) + return apiVs.NewPutVsVipPortRsHealthOK().WithPayload(rssModels) + case types.EDPVS_NOTEXIST: + if existOnly { + h.logger.Error("Edit not exist real server.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) + return apiVs.NewPutVsVipPortRsHealthInvalidFrontend() + } + h.logger.Error("Unreachable branch") + default: + h.logger.Error("Set real server sets failed.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) + return apiVs.NewPutVsVipPortRsHealthInvalidBackend() + } + return apiVs.NewPutVsVipPortRsHealthFailure() +} diff --git a/tools/dpvs-agent/dpvs-agent-api.yaml b/tools/dpvs-agent/dpvs-agent-api.yaml index bd4ff3340..0640a145c 100644 --- a/tools/dpvs-agent/dpvs-agent-api.yaml +++ b/tools/dpvs-agent/dpvs-agent-api.yaml @@ -100,6 +100,12 @@ parameters: type: boolean default: false required: false + snapshot: + name: snapshot + in: query + type: boolean + default: true + required: false stats: name: stats in: query @@ -148,7 +154,33 @@ parameters: - off default: unset required: false + version: + name: version + in: query + type: string + required: true definitions: + NodeServiceSnapshot: + type: object + properties: + NodeSpec: + "$ref": "#/definitions/DpvsNodeSpec" + Services: + "$ref": "#/definitions/VirtualServerList" + DpvsNodeSpec: + type: object + properties: + AnnouncePort: + "$ref": "#/definitions/VsAnnouncePort" + Laddrs: + "$ref": "#/definitions/LocalAddressExpandList" + VsAnnouncePort: + type: object + properties: + switch: + type: string + dpvs: + type: string CertAuthSpec: properties: addr: @@ -472,7 +504,6 @@ definitions: - tcp - udp - ping - VirtualServerList: type: object properties: @@ -539,6 +570,8 @@ definitions: format: "uint32" Addr: type: "string" + Version: + type: "string" SchedName: type: "string" enum: @@ -559,18 +592,9 @@ definitions: VirtualServerSpecTiny: type: "object" properties: - #Af: - # type: "integer" - # format: "uint16" - #Port: - # type: "integer" - # format: "uint16" Fwmark: type: "integer" format: "uint32" - #Flags: - # type: "integer" - # format: "uint32" SynProxy: type: "boolean" default: false @@ -583,9 +607,6 @@ definitions: ConnTimeout: type: "integer" format: "uint32" - #Netmask: - # type: "integer" - # format: "uint32" Bps: type: "integer" format: "uint32" @@ -647,6 +668,7 @@ paths: tags: - "device" parameters: + - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/sapool" - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/device-addr" @@ -803,6 +825,7 @@ paths: tags: - "device" parameters: + - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/device-addr" summary: "ip addr add 10.0.0.1/32 dev eth0: Set ip cird to linux net device" @@ -846,7 +869,6 @@ paths: '200': description: Success schema: - #type: string "$ref": "#/definitions/NicDeviceSpecList" '500': description: Failure @@ -925,19 +947,19 @@ paths: - "virtualserver" parameters: - "$ref": "#/parameters/stats" + - "$ref": "#/parameters/snapshot" summary: "display all vip:port:proto and rsip:port list" responses: '200': description: Success schema: "$ref": "#/definitions/VirtualServerList" - # items: - # "$ref": "#/definitions/VirtualServer" /vs/{VipPort}: get: tags: - "virtualserver" parameters: + - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/service-id" - "$ref": "#/parameters/stats" summary: "get a specific virtual server" @@ -945,8 +967,6 @@ paths: '200': description: Success schema: - # type: string - # items: "$ref": "#/definitions/VirtualServerList" '404': description: Service not found @@ -977,6 +997,7 @@ paths: tags: - "virtualserver" parameters: + - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/service-id" - "$ref": "#/parameters/vs-config" responses: @@ -1008,7 +1029,7 @@ paths: tags: - "virtualserver" parameters: - #- "$ref": "#/parameters/realserver-id" + - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/service-id" - "$ref": "#/parameters/stats" responses: @@ -1016,9 +1037,6 @@ paths: description: Success schema: "$ref": "#/definitions/LocalAddressExpandList" - #type: string - #items: - # "$ref": "#/definitions/VirtualServer" '404': description: Service not found schema: @@ -1027,6 +1045,7 @@ paths: tags: - "virtualserver" parameters: + - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/service-id" - "$ref": "#/parameters/laddr-config" responses: @@ -1079,12 +1098,46 @@ paths: x-go-name: Failure schema: "$ref": "#/definitions/Error" + /vs/{VipPort}/rs/health: + put: + summary: "dpvs healthcheck update rs weight" + tags: + - "virtualserver" + parameters: + - "$ref": "#/parameters/version" + - "$ref": "#/parameters/service-id" + - "$ref": "#/parameters/rss-config" + responses: + '200': + description: Success + schema: + "$ref": "#/definitions/RealServerExpandList" + '270': + description: "the rss-config parameter is outdated, update nothing and return the latest rs info" + x-go-name: Unexpected + schema: + "$ref": "#/definitions/RealServerExpandList" + '460': + description: Invalid frontend in service configuration + x-go-name: InvalidFrontend + schema: + "$ref": "#/definitions/Error" + '461': + description: Invalid backend in service configuration + x-go-name: InvalidBackend + schema: + "$ref": "#/definitions/Error" + '500': + description: Service deletion failed + x-go-name: Failure + schema: + "$ref": "#/definitions/Error" /vs/{VipPort}/rs: get: tags: - "virtualserver" parameters: - #- "$ref": "#/parameters/realserver-id" + - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/service-id" - "$ref": "#/parameters/stats" responses: @@ -1092,8 +1145,6 @@ paths: description: Success schema: type: string - #items: - # "$ref": "#/definitions/VirtualServer" '404': description: Service not found schema: @@ -1134,7 +1185,6 @@ paths: parameters: - "$ref": "#/parameters/service-id" - "$ref": "#/parameters/rss-config" - - "$ref": "#/parameters/healthcheck" responses: '200': description: Success @@ -1164,6 +1214,7 @@ paths: tags: - "virtualserver" parameters: + - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/service-id" - "$ref": "#/parameters/rss-config" responses: @@ -1195,15 +1246,12 @@ paths: tags: - "virtualserver" parameters: - #- "$ref": "#/parameters/realserver-id" - "$ref": "#/parameters/service-id" responses: '200': description: Success schema: type: string - #items: - # "$ref": "#/definitions/VirtualServer" '404': description: Service not found schema: @@ -1304,15 +1352,12 @@ paths: tags: - "virtualserver" parameters: - #- "$ref": "#/parameters/realserver-id" - "$ref": "#/parameters/service-id" responses: '200': description: Success schema: type: string - #items: - # "$ref": "#/definitions/VirtualServer" '404': description: Service not found schema: diff --git a/tools/dpvs-agent/models/dpvs_node_spec.go b/tools/dpvs-agent/models/dpvs_node_spec.go new file mode 100644 index 000000000..c8660bfaa --- /dev/null +++ b/tools/dpvs-agent/models/dpvs_node_spec.go @@ -0,0 +1,150 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// DpvsNodeSpec dpvs node spec +// +// swagger:model DpvsNodeSpec +type DpvsNodeSpec struct { + + // announce port + AnnouncePort *VsAnnouncePort `json:"AnnouncePort,omitempty"` + + // laddrs + Laddrs *LocalAddressExpandList `json:"Laddrs,omitempty"` +} + +// Validate validates this dpvs node spec +func (m *DpvsNodeSpec) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAnnouncePort(formats); err != nil { + res = append(res, err) + } + + if err := m.validateLaddrs(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *DpvsNodeSpec) validateAnnouncePort(formats strfmt.Registry) error { + if swag.IsZero(m.AnnouncePort) { // not required + return nil + } + + if m.AnnouncePort != nil { + if err := m.AnnouncePort.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("AnnouncePort") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("AnnouncePort") + } + return err + } + } + + return nil +} + +func (m *DpvsNodeSpec) validateLaddrs(formats strfmt.Registry) error { + if swag.IsZero(m.Laddrs) { // not required + return nil + } + + if m.Laddrs != nil { + if err := m.Laddrs.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Laddrs") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Laddrs") + } + return err + } + } + + return nil +} + +// ContextValidate validate this dpvs node spec based on the context it is used +func (m *DpvsNodeSpec) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateAnnouncePort(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateLaddrs(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *DpvsNodeSpec) contextValidateAnnouncePort(ctx context.Context, formats strfmt.Registry) error { + + if m.AnnouncePort != nil { + if err := m.AnnouncePort.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("AnnouncePort") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("AnnouncePort") + } + return err + } + } + + return nil +} + +func (m *DpvsNodeSpec) contextValidateLaddrs(ctx context.Context, formats strfmt.Registry) error { + + if m.Laddrs != nil { + if err := m.Laddrs.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Laddrs") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Laddrs") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *DpvsNodeSpec) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *DpvsNodeSpec) UnmarshalBinary(b []byte) error { + var res DpvsNodeSpec + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/tools/dpvs-agent/models/node_service_snapshot.go b/tools/dpvs-agent/models/node_service_snapshot.go new file mode 100644 index 000000000..148038e75 --- /dev/null +++ b/tools/dpvs-agent/models/node_service_snapshot.go @@ -0,0 +1,150 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NodeServiceSnapshot node service snapshot +// +// swagger:model NodeServiceSnapshot +type NodeServiceSnapshot struct { + + // node spec + NodeSpec *DpvsNodeSpec `json:"NodeSpec,omitempty"` + + // services + Services *VirtualServerList `json:"Services,omitempty"` +} + +// Validate validates this node service snapshot +func (m *NodeServiceSnapshot) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateNodeSpec(formats); err != nil { + res = append(res, err) + } + + if err := m.validateServices(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *NodeServiceSnapshot) validateNodeSpec(formats strfmt.Registry) error { + if swag.IsZero(m.NodeSpec) { // not required + return nil + } + + if m.NodeSpec != nil { + if err := m.NodeSpec.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("NodeSpec") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("NodeSpec") + } + return err + } + } + + return nil +} + +func (m *NodeServiceSnapshot) validateServices(formats strfmt.Registry) error { + if swag.IsZero(m.Services) { // not required + return nil + } + + if m.Services != nil { + if err := m.Services.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Services") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Services") + } + return err + } + } + + return nil +} + +// ContextValidate validate this node service snapshot based on the context it is used +func (m *NodeServiceSnapshot) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateNodeSpec(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateServices(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *NodeServiceSnapshot) contextValidateNodeSpec(ctx context.Context, formats strfmt.Registry) error { + + if m.NodeSpec != nil { + if err := m.NodeSpec.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("NodeSpec") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("NodeSpec") + } + return err + } + } + + return nil +} + +func (m *NodeServiceSnapshot) contextValidateServices(ctx context.Context, formats strfmt.Registry) error { + + if m.Services != nil { + if err := m.Services.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Services") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Services") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *NodeServiceSnapshot) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *NodeServiceSnapshot) UnmarshalBinary(b []byte) error { + var res NodeServiceSnapshot + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/tools/dpvs-agent/models/virtual_server_spec_expand.go b/tools/dpvs-agent/models/virtual_server_spec_expand.go index fbd31358d..ce917ce61 100644 --- a/tools/dpvs-agent/models/virtual_server_spec_expand.go +++ b/tools/dpvs-agent/models/virtual_server_spec_expand.go @@ -86,6 +86,9 @@ type VirtualServerSpecExpand struct { // timeout Timeout uint32 `json:"Timeout,omitempty"` + + // version + Version string `json:"Version,omitempty"` } // Validate validates this virtual server spec expand diff --git a/tools/dpvs-agent/models/vs_announce_port.go b/tools/dpvs-agent/models/vs_announce_port.go new file mode 100644 index 000000000..8445066d8 --- /dev/null +++ b/tools/dpvs-agent/models/vs_announce_port.go @@ -0,0 +1,53 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// VsAnnouncePort vs announce port +// +// swagger:model VsAnnouncePort +type VsAnnouncePort struct { + + // dpvs + Dpvs string `json:"dpvs,omitempty"` + + // switch + Switch string `json:"switch,omitempty"` +} + +// Validate validates this vs announce port +func (m *VsAnnouncePort) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this vs announce port based on context it is used +func (m *VsAnnouncePort) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *VsAnnouncePort) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *VsAnnouncePort) UnmarshalBinary(b []byte) error { + var res VsAnnouncePort + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/tools/dpvs-agent/pkg/ipc/types/snapshot.go b/tools/dpvs-agent/pkg/ipc/types/snapshot.go new file mode 100644 index 000000000..4491d82c9 --- /dev/null +++ b/tools/dpvs-agent/pkg/ipc/types/snapshot.go @@ -0,0 +1,130 @@ +package types + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "time" + + "golang.org/x/sys/unix" + + "github.com/dpvs-agent/models" + "github.com/hashicorp/go-hclog" +) + +type NodeSnapshot struct { + NodeSpec *models.DpvsNodeSpec + Services map[string]*models.VirtualServerSpecExpand +} + +func (snapshot *NodeSnapshot) ServiceVersionUpdate(id string, logger hclog.Logger) { + services := snapshot.Services + logger.Info("Update server version begin.", "id", id, "services", services) + if _, exist := services[strings.ToLower(id)]; exist { + services[strings.ToLower(id)].Version = strconv.FormatInt(time.Now().UnixNano()/1e6, 10) + return + } + logger.Error("Update service version failed.", "id", id) +} + +func (snapshot *NodeSnapshot) ServiceDel(id string) { + if _, exist := snapshot.Services[strings.ToLower(id)]; exist { + delete(snapshot.Services, strings.ToLower(id)) + } +} + +func (snapshot *NodeSnapshot) ServiceVersion(id string) string { + if _, exist := snapshot.Services[strings.ToLower(id)]; exist { + return snapshot.Services[strings.ToLower(id)].Version + } + return strconv.FormatInt(time.Now().UnixNano()/1e6, 10) +} + +func (snapshot *NodeSnapshot) ServiceAdd(vs *VirtualServerSpec) { + version := snapshot.ServiceVersion(vs.ID()) + + snapshot.Services[strings.ToLower(vs.ID())] = vs.GetModel() + + snapshot.Services[strings.ToLower(vs.ID())].Version = version +} + +func (snapshot *NodeSnapshot) ServiceUpsert(spec *models.VirtualServerSpecExpand) { + svc := (*VirtualServerSpecExpandModel)(spec) + + version := snapshot.ServiceVersion(svc.ID()) + + snapshot.Services[strings.ToLower(svc.ID())] = spec + + snapshot.Services[strings.ToLower(svc.ID())].Version = version +} + +func (snapshot *NodeSnapshot) GetModels(logger hclog.Logger) *models.VirtualServerList { + services := &models.VirtualServerList{Items: make([]*models.VirtualServerSpecExpand, len(snapshot.Services))} + i := 0 + for _, svc := range snapshot.Services { + services.Items[i] = svc + i++ + } + return services +} + +type VirtualServerSpecExpandModel models.VirtualServerSpecExpand + +func (spec *VirtualServerSpecExpandModel) ID() string { + proto := "tcp" + if spec.Proto == unix.IPPROTO_UDP { + proto = "udp" + } + return fmt.Sprintf("%s-%d-%s", spec.Addr, spec.Port, proto) +} + +func (snapshot *NodeSnapshot) LoadFrom(cacheFile string, logger hclog.Logger) error { + content, err := os.ReadFile(cacheFile) + if err != nil { + logger.Error("Read dpvs service cache file failed.", "Error", err.Error()) + return err + } + var nodeSnapshot models.NodeServiceSnapshot + if err := json.Unmarshal(content, &nodeSnapshot); err != nil { + logger.Error("Deserialization Failed.", "content", content, "Error", err.Error()) + return err + } + + snapshot.NodeSpec = nodeSnapshot.NodeSpec + for _, svcModel := range nodeSnapshot.Services.Items { + svc := (*VirtualServerSpecExpandModel)(svcModel) + snapshot.Services[strings.ToLower(svc.ID())] = svcModel + } + + return nil +} + +func (snapshot *NodeSnapshot) DumpTo(cacheFile string, logger hclog.Logger) error { + nodeSnapshot := &models.NodeServiceSnapshot{ + NodeSpec: snapshot.NodeSpec, + Services: snapshot.GetModels(logger), + } + + content, err := json.Marshal(nodeSnapshot) + if err != nil { + logger.Error(err.Error()) + return err + } + + nowStr := time.Now().Format("2006-01-02 15:04:05") + nowStr = strings.ReplaceAll(nowStr, " ", "+") + bakName := cacheFile + nowStr + if err := os.Rename(cacheFile, bakName); err != nil { + logger.Error(err.Error()) + return err + } + + if err := os.WriteFile(cacheFile, []byte(content), 0644); err != nil { + logger.Error(err.Error()) + return err + } + + return nil +} diff --git a/tools/dpvs-agent/pkg/settings/settings.go b/tools/dpvs-agent/pkg/settings/settings.go new file mode 100644 index 000000000..5027d4344 --- /dev/null +++ b/tools/dpvs-agent/pkg/settings/settings.go @@ -0,0 +1,43 @@ +package settings + +import ( + "sync" + + "github.com/dpvs-agent/models" + "github.com/dpvs-agent/pkg/ipc/types" +) + +var ( + shareSnapshot *types.NodeSnapshot + shareAppConfig *AppConfig + initOnce sync.Once +) + +type AppConfig struct { + CacheFile string +} + +func setUp() { + shareAppConfig = &AppConfig{} + shareSnapshot = &types.NodeSnapshot{ + NodeSpec: &models.DpvsNodeSpec{ + AnnouncePort: &models.VsAnnouncePort{}, + }, + Services: make(map[string]*models.VirtualServerSpecExpand), + } +} + +func ShareAppConfig() *AppConfig { + initOnce.Do(setUp) + return shareAppConfig +} + +func ShareSnapshot() *types.NodeSnapshot { + initOnce.Do(setUp) + return shareSnapshot +} + +func LocalConfigFile() string { + // return filepath.Join(shareAppConfig.ConfigDir, "cache") + return shareAppConfig.CacheFile +} diff --git a/tools/dpvs-agent/pkg/settings/util.go b/tools/dpvs-agent/pkg/settings/util.go new file mode 100644 index 000000000..73296b7de --- /dev/null +++ b/tools/dpvs-agent/pkg/settings/util.go @@ -0,0 +1,18 @@ +package settings + +import ( + "errors" + "fmt" + "strings" +) + +func MergedError(errs []error) error { + if len(errs) <= 0 { + return nil + } + var msg []string + for _, e := range errs { + msg = append(msg, e.Error()) + } + return errors.New(fmt.Sprintf("errors: %s", strings.Join(msg, "\n"))) +} diff --git a/tools/dpvs-agent/restapi/embedded_spec.go b/tools/dpvs-agent/restapi/embedded_spec.go index 7c6773a97..f55b028fe 100644 --- a/tools/dpvs-agent/restapi/embedded_spec.go +++ b/tools/dpvs-agent/restapi/embedded_spec.go @@ -89,6 +89,9 @@ func init() { ], "summary": "add/update special net device ip addr", "parameters": [ + { + "$ref": "#/parameters/snapshot" + }, { "$ref": "#/parameters/sapool" }, @@ -267,6 +270,9 @@ func init() { ], "summary": "ip addr add 10.0.0.1/32 dev eth0: Set ip cird to linux net device", "parameters": [ + { + "$ref": "#/parameters/snapshot" + }, { "$ref": "#/parameters/device-name" }, @@ -569,6 +575,9 @@ func init() { "parameters": [ { "$ref": "#/parameters/stats" + }, + { + "$ref": "#/parameters/snapshot" } ], "responses": { @@ -588,6 +597,9 @@ func init() { ], "summary": "get a specific virtual server", "parameters": [ + { + "$ref": "#/parameters/snapshot" + }, { "$ref": "#/parameters/service-id" }, @@ -616,6 +628,9 @@ func init() { ], "summary": "create or update virtual server", "parameters": [ + { + "$ref": "#/parameters/snapshot" + }, { "$ref": "#/parameters/service-id" }, @@ -1038,6 +1053,9 @@ func init() { "virtualserver" ], "parameters": [ + { + "$ref": "#/parameters/snapshot" + }, { "$ref": "#/parameters/service-id" }, @@ -1065,6 +1083,9 @@ func init() { "virtualserver" ], "parameters": [ + { + "$ref": "#/parameters/snapshot" + }, { "$ref": "#/parameters/service-id" }, @@ -1157,6 +1178,9 @@ func init() { "virtualserver" ], "parameters": [ + { + "$ref": "#/parameters/snapshot" + }, { "$ref": "#/parameters/service-id" }, @@ -1190,9 +1214,6 @@ func init() { }, { "$ref": "#/parameters/rss-config" - }, - { - "$ref": "#/parameters/healthcheck" } ], "responses": { @@ -1237,6 +1258,9 @@ func init() { ], "summary": "Update fully real server list to vip:port:proto", "parameters": [ + { + "$ref": "#/parameters/snapshot" + }, { "$ref": "#/parameters/service-id" }, @@ -1326,6 +1350,61 @@ func init() { } } } + }, + "/vs/{VipPort}/rs/health": { + "put": { + "tags": [ + "virtualserver" + ], + "summary": "dpvs healthcheck update rs weight", + "parameters": [ + { + "$ref": "#/parameters/version" + }, + { + "$ref": "#/parameters/service-id" + }, + { + "$ref": "#/parameters/rss-config" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/RealServerExpandList" + } + }, + "270": { + "description": "the rss-config parameter is outdated, update nothing and return the latest rs info", + "schema": { + "$ref": "#/definitions/RealServerExpandList" + }, + "x-go-name": "Unexpected" + }, + "460": { + "description": "Invalid frontend in service configuration", + "schema": { + "$ref": "#/definitions/Error" + }, + "x-go-name": "InvalidFrontend" + }, + "461": { + "description": "Invalid backend in service configuration", + "schema": { + "$ref": "#/definitions/Error" + }, + "x-go-name": "InvalidBackend" + }, + "500": { + "description": "Service deletion failed", + "schema": { + "$ref": "#/definitions/Error" + }, + "x-go-name": "Failure" + } + } + } } }, "definitions": { @@ -1367,6 +1446,17 @@ func init() { "ping" ] }, + "DpvsNodeSpec": { + "type": "object", + "properties": { + "AnnouncePort": { + "$ref": "#/definitions/VsAnnouncePort" + }, + "Laddrs": { + "$ref": "#/definitions/LocalAddressExpandList" + } + } + }, "Error": { "type": "string" }, @@ -1618,6 +1708,17 @@ func init() { } } }, + "NodeServiceSnapshot": { + "type": "object", + "properties": { + "NodeSpec": { + "$ref": "#/definitions/DpvsNodeSpec" + }, + "Services": { + "$ref": "#/definitions/VirtualServerList" + } + } + }, "RealServerExpandList": { "type": "object", "properties": { @@ -1887,6 +1988,9 @@ func init() { "Timeout": { "type": "integer", "format": "uint32" + }, + "Version": { + "type": "string" } } }, @@ -1957,6 +2061,17 @@ func init() { "type": "string" } } + }, + "VsAnnouncePort": { + "type": "object", + "properties": { + "dpvs": { + "type": "string" + }, + "switch": { + "type": "string" + } + } } }, "parameters": { @@ -2065,6 +2180,12 @@ func init() { "in": "path", "required": true }, + "snapshot": { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" + }, "stats": { "type": "boolean", "default": false, @@ -2077,6 +2198,12 @@ func init() { "name": "verbose", "in": "query" }, + "version": { + "type": "string", + "name": "version", + "in": "query", + "required": true + }, "vlan-config": { "name": "spec", "in": "body", @@ -2211,6 +2338,12 @@ func init() { ], "summary": "add/update special net device ip addr", "parameters": [ + { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" + }, { "type": "boolean", "default": false, @@ -2427,6 +2560,12 @@ func init() { ], "summary": "ip addr add 10.0.0.1/32 dev eth0: Set ip cird to linux net device", "parameters": [ + { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" + }, { "type": "string", "name": "name", @@ -2818,6 +2957,12 @@ func init() { "default": false, "name": "stats", "in": "query" + }, + { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" } ], "responses": { @@ -2837,6 +2982,12 @@ func init() { ], "summary": "get a specific virtual server", "parameters": [ + { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" + }, { "type": "string", "name": "VipPort", @@ -2871,6 +3022,12 @@ func init() { ], "summary": "create or update virtual server", "parameters": [ + { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" + }, { "type": "string", "name": "VipPort", @@ -3351,6 +3508,12 @@ func init() { "virtualserver" ], "parameters": [ + { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" + }, { "type": "string", "name": "VipPort", @@ -3384,6 +3547,12 @@ func init() { "virtualserver" ], "parameters": [ + { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" + }, { "type": "string", "name": "VipPort", @@ -3490,6 +3659,12 @@ func init() { "virtualserver" ], "parameters": [ + { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" + }, { "type": "string", "name": "VipPort", @@ -3536,12 +3711,6 @@ func init() { "schema": { "$ref": "#/definitions/RealServerTinyList" } - }, - { - "type": "boolean", - "default": false, - "name": "healthcheck", - "in": "query" } ], "responses": { @@ -3586,6 +3755,12 @@ func init() { ], "summary": "Update fully real server list to vip:port:proto", "parameters": [ + { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" + }, { "type": "string", "name": "VipPort", @@ -3689,6 +3864,71 @@ func init() { } } } + }, + "/vs/{VipPort}/rs/health": { + "put": { + "tags": [ + "virtualserver" + ], + "summary": "dpvs healthcheck update rs weight", + "parameters": [ + { + "type": "string", + "name": "version", + "in": "query", + "required": true + }, + { + "type": "string", + "name": "VipPort", + "in": "path", + "required": true + }, + { + "name": "rss", + "in": "body", + "schema": { + "$ref": "#/definitions/RealServerTinyList" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/RealServerExpandList" + } + }, + "270": { + "description": "the rss-config parameter is outdated, update nothing and return the latest rs info", + "schema": { + "$ref": "#/definitions/RealServerExpandList" + }, + "x-go-name": "Unexpected" + }, + "460": { + "description": "Invalid frontend in service configuration", + "schema": { + "$ref": "#/definitions/Error" + }, + "x-go-name": "InvalidFrontend" + }, + "461": { + "description": "Invalid backend in service configuration", + "schema": { + "$ref": "#/definitions/Error" + }, + "x-go-name": "InvalidBackend" + }, + "500": { + "description": "Service deletion failed", + "schema": { + "$ref": "#/definitions/Error" + }, + "x-go-name": "Failure" + } + } + } } }, "definitions": { @@ -3730,6 +3970,17 @@ func init() { "ping" ] }, + "DpvsNodeSpec": { + "type": "object", + "properties": { + "AnnouncePort": { + "$ref": "#/definitions/VsAnnouncePort" + }, + "Laddrs": { + "$ref": "#/definitions/LocalAddressExpandList" + } + } + }, "Error": { "type": "string" }, @@ -3981,6 +4232,17 @@ func init() { } } }, + "NodeServiceSnapshot": { + "type": "object", + "properties": { + "NodeSpec": { + "$ref": "#/definitions/DpvsNodeSpec" + }, + "Services": { + "$ref": "#/definitions/VirtualServerList" + } + } + }, "RealServerExpandList": { "type": "object", "properties": { @@ -4250,6 +4512,9 @@ func init() { "Timeout": { "type": "integer", "format": "uint32" + }, + "Version": { + "type": "string" } } }, @@ -4320,6 +4585,17 @@ func init() { "type": "string" } } + }, + "VsAnnouncePort": { + "type": "object", + "properties": { + "dpvs": { + "type": "string" + }, + "switch": { + "type": "string" + } + } } }, "parameters": { @@ -4428,6 +4704,12 @@ func init() { "in": "path", "required": true }, + "snapshot": { + "type": "boolean", + "default": true, + "name": "snapshot", + "in": "query" + }, "stats": { "type": "boolean", "default": false, @@ -4440,6 +4722,12 @@ func init() { "name": "verbose", "in": "query" }, + "version": { + "type": "string", + "name": "version", + "in": "query", + "required": true + }, "vlan-config": { "name": "spec", "in": "body", diff --git a/tools/dpvs-agent/restapi/operations/device/put_device_name_addr_parameters.go b/tools/dpvs-agent/restapi/operations/device/put_device_name_addr_parameters.go index 07bc411bb..68e2a817b 100644 --- a/tools/dpvs-agent/restapi/operations/device/put_device_name_addr_parameters.go +++ b/tools/dpvs-agent/restapi/operations/device/put_device_name_addr_parameters.go @@ -25,11 +25,14 @@ func NewPutDeviceNameAddrParams() PutDeviceNameAddrParams { var ( // initialize parameters with default values - sapoolDefault = bool(false) + sapoolDefault = bool(false) + snapshotDefault = bool(true) ) return PutDeviceNameAddrParams{ Sapool: &sapoolDefault, + + Snapshot: &snapshotDefault, } } @@ -52,6 +55,11 @@ type PutDeviceNameAddrParams struct { Default: false */ Sapool *bool + /* + In: query + Default: true + */ + Snapshot *bool /* In: body */ @@ -79,6 +87,11 @@ func (o *PutDeviceNameAddrParams) BindRequest(r *http.Request, route *middleware res = append(res, err) } + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") + if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { + res = append(res, err) + } + if runtime.HasBody(r) { defer r.Body.Close() var body models.InetAddrSpec @@ -143,3 +156,27 @@ func (o *PutDeviceNameAddrParams) bindSapool(rawData []string, hasKey bool, form return nil } + +// bindSnapshot binds and validates parameter Snapshot from query. +func (o *PutDeviceNameAddrParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewPutDeviceNameAddrParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("snapshot", "query", "bool", raw) + } + o.Snapshot = &value + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/device/put_device_name_addr_urlbuilder.go b/tools/dpvs-agent/restapi/operations/device/put_device_name_addr_urlbuilder.go index a136e39c6..5374db59c 100644 --- a/tools/dpvs-agent/restapi/operations/device/put_device_name_addr_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/device/put_device_name_addr_urlbuilder.go @@ -18,7 +18,8 @@ import ( type PutDeviceNameAddrURL struct { Name string - Sapool *bool + Sapool *bool + Snapshot *bool _basePath string // avoid unkeyed usage @@ -69,6 +70,14 @@ func (o *PutDeviceNameAddrURL) Build() (*url.URL, error) { qs.Set("sapool", sapoolQ) } + var snapshotQ string + if o.Snapshot != nil { + snapshotQ = swag.FormatBool(*o.Snapshot) + } + if snapshotQ != "" { + qs.Set("snapshot", snapshotQ) + } + _result.RawQuery = qs.Encode() return &_result, nil diff --git a/tools/dpvs-agent/restapi/operations/device/put_device_name_netlink_addr_parameters.go b/tools/dpvs-agent/restapi/operations/device/put_device_name_netlink_addr_parameters.go index 7fcd77b11..4aceb7bec 100644 --- a/tools/dpvs-agent/restapi/operations/device/put_device_name_netlink_addr_parameters.go +++ b/tools/dpvs-agent/restapi/operations/device/put_device_name_netlink_addr_parameters.go @@ -12,17 +12,25 @@ import ( "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/go-openapi/validate" "github.com/dpvs-agent/models" ) // NewPutDeviceNameNetlinkAddrParams creates a new PutDeviceNameNetlinkAddrParams object -// -// There are no default values defined in the spec. +// with the default values initialized. func NewPutDeviceNameNetlinkAddrParams() PutDeviceNameNetlinkAddrParams { - return PutDeviceNameNetlinkAddrParams{} + var ( + // initialize parameters with default values + + snapshotDefault = bool(true) + ) + + return PutDeviceNameNetlinkAddrParams{ + Snapshot: &snapshotDefault, + } } // PutDeviceNameNetlinkAddrParams contains all the bound params for the put device name netlink addr operation @@ -39,6 +47,11 @@ type PutDeviceNameNetlinkAddrParams struct { In: path */ Name string + /* + In: query + Default: true + */ + Snapshot *bool /* In: body */ @@ -54,11 +67,18 @@ func (o *PutDeviceNameNetlinkAddrParams) BindRequest(r *http.Request, route *mid o.HTTPRequest = r + qs := runtime.Values(r.URL.Query()) + rName, rhkName, _ := route.Params.GetOK("name") if err := o.bindName(rName, rhkName, route.Formats); err != nil { res = append(res, err) } + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") + if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { + res = append(res, err) + } + if runtime.HasBody(r) { defer r.Body.Close() var body models.InetAddrSpec @@ -99,3 +119,27 @@ func (o *PutDeviceNameNetlinkAddrParams) bindName(rawData []string, hasKey bool, return nil } + +// bindSnapshot binds and validates parameter Snapshot from query. +func (o *PutDeviceNameNetlinkAddrParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewPutDeviceNameNetlinkAddrParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("snapshot", "query", "bool", raw) + } + o.Snapshot = &value + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/device/put_device_name_netlink_addr_urlbuilder.go b/tools/dpvs-agent/restapi/operations/device/put_device_name_netlink_addr_urlbuilder.go index ef47a83b2..e47ed59b4 100644 --- a/tools/dpvs-agent/restapi/operations/device/put_device_name_netlink_addr_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/device/put_device_name_netlink_addr_urlbuilder.go @@ -10,12 +10,16 @@ import ( "net/url" golangswaggerpaths "path" "strings" + + "github.com/go-openapi/swag" ) // PutDeviceNameNetlinkAddrURL generates an URL for the put device name netlink addr operation type PutDeviceNameNetlinkAddrURL struct { Name string + Snapshot *bool + _basePath string // avoid unkeyed usage _ struct{} @@ -55,6 +59,18 @@ func (o *PutDeviceNameNetlinkAddrURL) Build() (*url.URL, error) { } _result.Path = golangswaggerpaths.Join(_basePath, _path) + qs := make(url.Values) + + var snapshotQ string + if o.Snapshot != nil { + snapshotQ = swag.FormatBool(*o.Snapshot) + } + if snapshotQ != "" { + qs.Set("snapshot", snapshotQ) + } + + _result.RawQuery = qs.Encode() + return &_result, nil } diff --git a/tools/dpvs-agent/restapi/operations/dpvs_agent_api.go b/tools/dpvs-agent/restapi/operations/dpvs_agent_api.go index 5421e4aa7..8f292b4ba 100644 --- a/tools/dpvs-agent/restapi/operations/dpvs_agent_api.go +++ b/tools/dpvs-agent/restapi/operations/dpvs_agent_api.go @@ -156,6 +156,9 @@ func NewDpvsAgentAPI(spec *loads.Document) *DpvsAgentAPI { VirtualserverPutVsVipPortRsHandler: virtualserver.PutVsVipPortRsHandlerFunc(func(params virtualserver.PutVsVipPortRsParams) middleware.Responder { return middleware.NotImplemented("operation virtualserver.PutVsVipPortRs has not yet been implemented") }), + VirtualserverPutVsVipPortRsHealthHandler: virtualserver.PutVsVipPortRsHealthHandlerFunc(func(params virtualserver.PutVsVipPortRsHealthParams) middleware.Responder { + return middleware.NotImplemented("operation virtualserver.PutVsVipPortRsHealth has not yet been implemented") + }), } } @@ -266,6 +269,8 @@ type DpvsAgentAPI struct { VirtualserverPutVsVipPortLaddrHandler virtualserver.PutVsVipPortLaddrHandler // VirtualserverPutVsVipPortRsHandler sets the operation handler for the put vs vip port rs operation VirtualserverPutVsVipPortRsHandler virtualserver.PutVsVipPortRsHandler + // VirtualserverPutVsVipPortRsHealthHandler sets the operation handler for the put vs vip port rs health operation + VirtualserverPutVsVipPortRsHealthHandler virtualserver.PutVsVipPortRsHealthHandler // ServeError is called when an error is received, there is a default handler // but you can set your own with this @@ -454,6 +459,9 @@ func (o *DpvsAgentAPI) Validate() error { if o.VirtualserverPutVsVipPortRsHandler == nil { unregistered = append(unregistered, "virtualserver.PutVsVipPortRsHandler") } + if o.VirtualserverPutVsVipPortRsHealthHandler == nil { + unregistered = append(unregistered, "virtualserver.PutVsVipPortRsHealthHandler") + } if len(unregistered) > 0 { return fmt.Errorf("missing registration: %s", strings.Join(unregistered, ", ")) @@ -690,6 +698,10 @@ func (o *DpvsAgentAPI) initHandlerCache() { o.handlers["PUT"] = make(map[string]http.Handler) } o.handlers["PUT"]["/vs/{VipPort}/rs"] = virtualserver.NewPutVsVipPortRs(o.context, o.VirtualserverPutVsVipPortRsHandler) + if o.handlers["PUT"] == nil { + o.handlers["PUT"] = make(map[string]http.Handler) + } + o.handlers["PUT"]["/vs/{VipPort}/rs/health"] = virtualserver.NewPutVsVipPortRsHealth(o.context, o.VirtualserverPutVsVipPortRsHealthHandler) } // Serve creates a http handler to serve the API over HTTP diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_parameters.go index ac28ba465..4c527457b 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_parameters.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_parameters.go @@ -22,10 +22,13 @@ func NewGetVsParams() GetVsParams { var ( // initialize parameters with default values - statsDefault = bool(false) + snapshotDefault = bool(true) + statsDefault = bool(false) ) return GetVsParams{ + Snapshot: &snapshotDefault, + Stats: &statsDefault, } } @@ -39,6 +42,11 @@ type GetVsParams struct { // HTTP Request Object HTTPRequest *http.Request `json:"-"` + /* + In: query + Default: true + */ + Snapshot *bool /* In: query Default: false @@ -57,6 +65,11 @@ func (o *GetVsParams) BindRequest(r *http.Request, route *middleware.MatchedRout qs := runtime.Values(r.URL.Query()) + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") + if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { + res = append(res, err) + } + qStats, qhkStats, _ := qs.GetOK("stats") if err := o.bindStats(qStats, qhkStats, route.Formats); err != nil { res = append(res, err) @@ -67,6 +80,30 @@ func (o *GetVsParams) BindRequest(r *http.Request, route *middleware.MatchedRout return nil } +// bindSnapshot binds and validates parameter Snapshot from query. +func (o *GetVsParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetVsParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("snapshot", "query", "bool", raw) + } + o.Snapshot = &value + + return nil +} + // bindStats binds and validates parameter Stats from query. func (o *GetVsParams) bindStats(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_urlbuilder.go index 917308dcf..35fc9b8f6 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_urlbuilder.go @@ -15,7 +15,8 @@ import ( // GetVsURL generates an URL for the get vs operation type GetVsURL struct { - Stats *bool + Snapshot *bool + Stats *bool _basePath string // avoid unkeyed usage @@ -51,6 +52,14 @@ func (o *GetVsURL) Build() (*url.URL, error) { qs := make(url.Values) + var snapshotQ string + if o.Snapshot != nil { + snapshotQ = swag.FormatBool(*o.Snapshot) + } + if snapshotQ != "" { + qs.Set("snapshot", snapshotQ) + } + var statsQ string if o.Stats != nil { statsQ = swag.FormatBool(*o.Stats) diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_laddr_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_laddr_parameters.go index 0fb9cf281..37a8d01a6 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_laddr_parameters.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_laddr_parameters.go @@ -22,10 +22,13 @@ func NewGetVsVipPortLaddrParams() GetVsVipPortLaddrParams { var ( // initialize parameters with default values - statsDefault = bool(false) + snapshotDefault = bool(true) + statsDefault = bool(false) ) return GetVsVipPortLaddrParams{ + Snapshot: &snapshotDefault, + Stats: &statsDefault, } } @@ -44,6 +47,11 @@ type GetVsVipPortLaddrParams struct { In: path */ VipPort string + /* + In: query + Default: true + */ + Snapshot *bool /* In: query Default: false @@ -67,6 +75,11 @@ func (o *GetVsVipPortLaddrParams) BindRequest(r *http.Request, route *middleware res = append(res, err) } + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") + if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { + res = append(res, err) + } + qStats, qhkStats, _ := qs.GetOK("stats") if err := o.bindStats(qStats, qhkStats, route.Formats); err != nil { res = append(res, err) @@ -91,6 +104,30 @@ func (o *GetVsVipPortLaddrParams) bindVipPort(rawData []string, hasKey bool, for return nil } +// bindSnapshot binds and validates parameter Snapshot from query. +func (o *GetVsVipPortLaddrParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetVsVipPortLaddrParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("snapshot", "query", "bool", raw) + } + o.Snapshot = &value + + return nil +} + // bindStats binds and validates parameter Stats from query. func (o *GetVsVipPortLaddrParams) bindStats(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_laddr_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_laddr_urlbuilder.go index 053283e75..c71f2fa33 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_laddr_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_laddr_urlbuilder.go @@ -18,7 +18,8 @@ import ( type GetVsVipPortLaddrURL struct { VipPort string - Stats *bool + Snapshot *bool + Stats *bool _basePath string // avoid unkeyed usage @@ -61,6 +62,14 @@ func (o *GetVsVipPortLaddrURL) Build() (*url.URL, error) { qs := make(url.Values) + var snapshotQ string + if o.Snapshot != nil { + snapshotQ = swag.FormatBool(*o.Snapshot) + } + if snapshotQ != "" { + qs.Set("snapshot", snapshotQ) + } + var statsQ string if o.Stats != nil { statsQ = swag.FormatBool(*o.Stats) diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_parameters.go index a917c108a..77ef69f53 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_parameters.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_parameters.go @@ -22,10 +22,13 @@ func NewGetVsVipPortParams() GetVsVipPortParams { var ( // initialize parameters with default values - statsDefault = bool(false) + snapshotDefault = bool(true) + statsDefault = bool(false) ) return GetVsVipPortParams{ + Snapshot: &snapshotDefault, + Stats: &statsDefault, } } @@ -44,6 +47,11 @@ type GetVsVipPortParams struct { In: path */ VipPort string + /* + In: query + Default: true + */ + Snapshot *bool /* In: query Default: false @@ -67,6 +75,11 @@ func (o *GetVsVipPortParams) BindRequest(r *http.Request, route *middleware.Matc res = append(res, err) } + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") + if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { + res = append(res, err) + } + qStats, qhkStats, _ := qs.GetOK("stats") if err := o.bindStats(qStats, qhkStats, route.Formats); err != nil { res = append(res, err) @@ -91,6 +104,30 @@ func (o *GetVsVipPortParams) bindVipPort(rawData []string, hasKey bool, formats return nil } +// bindSnapshot binds and validates parameter Snapshot from query. +func (o *GetVsVipPortParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetVsVipPortParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("snapshot", "query", "bool", raw) + } + o.Snapshot = &value + + return nil +} + // bindStats binds and validates parameter Stats from query. func (o *GetVsVipPortParams) bindStats(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_rs_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_rs_parameters.go index 422d711e1..a7d68d0d3 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_rs_parameters.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_rs_parameters.go @@ -22,10 +22,13 @@ func NewGetVsVipPortRsParams() GetVsVipPortRsParams { var ( // initialize parameters with default values - statsDefault = bool(false) + snapshotDefault = bool(true) + statsDefault = bool(false) ) return GetVsVipPortRsParams{ + Snapshot: &snapshotDefault, + Stats: &statsDefault, } } @@ -44,6 +47,11 @@ type GetVsVipPortRsParams struct { In: path */ VipPort string + /* + In: query + Default: true + */ + Snapshot *bool /* In: query Default: false @@ -67,6 +75,11 @@ func (o *GetVsVipPortRsParams) BindRequest(r *http.Request, route *middleware.Ma res = append(res, err) } + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") + if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { + res = append(res, err) + } + qStats, qhkStats, _ := qs.GetOK("stats") if err := o.bindStats(qStats, qhkStats, route.Formats); err != nil { res = append(res, err) @@ -91,6 +104,30 @@ func (o *GetVsVipPortRsParams) bindVipPort(rawData []string, hasKey bool, format return nil } +// bindSnapshot binds and validates parameter Snapshot from query. +func (o *GetVsVipPortRsParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetVsVipPortRsParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("snapshot", "query", "bool", raw) + } + o.Snapshot = &value + + return nil +} + // bindStats binds and validates parameter Stats from query. func (o *GetVsVipPortRsParams) bindStats(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_rs_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_rs_urlbuilder.go index f97a471ed..30936c327 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_rs_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_rs_urlbuilder.go @@ -18,7 +18,8 @@ import ( type GetVsVipPortRsURL struct { VipPort string - Stats *bool + Snapshot *bool + Stats *bool _basePath string // avoid unkeyed usage @@ -61,6 +62,14 @@ func (o *GetVsVipPortRsURL) Build() (*url.URL, error) { qs := make(url.Values) + var snapshotQ string + if o.Snapshot != nil { + snapshotQ = swag.FormatBool(*o.Snapshot) + } + if snapshotQ != "" { + qs.Set("snapshot", snapshotQ) + } + var statsQ string if o.Stats != nil { statsQ = swag.FormatBool(*o.Stats) diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_urlbuilder.go index 699572745..0344bdc38 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_urlbuilder.go @@ -18,7 +18,8 @@ import ( type GetVsVipPortURL struct { VipPort string - Stats *bool + Snapshot *bool + Stats *bool _basePath string // avoid unkeyed usage @@ -61,6 +62,14 @@ func (o *GetVsVipPortURL) Build() (*url.URL, error) { qs := make(url.Values) + var snapshotQ string + if o.Snapshot != nil { + snapshotQ = swag.FormatBool(*o.Snapshot) + } + if snapshotQ != "" { + qs.Set("snapshot", snapshotQ) + } + var statsQ string if o.Stats != nil { statsQ = swag.FormatBool(*o.Stats) diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/post_vs_vip_port_rs_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/post_vs_vip_port_rs_parameters.go index 948a9f38b..b92f98b6f 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/post_vs_vip_port_rs_parameters.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/post_vs_vip_port_rs_parameters.go @@ -12,17 +12,25 @@ import ( "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/go-openapi/validate" "github.com/dpvs-agent/models" ) // NewPostVsVipPortRsParams creates a new PostVsVipPortRsParams object -// -// There are no default values defined in the spec. +// with the default values initialized. func NewPostVsVipPortRsParams() PostVsVipPortRsParams { - return PostVsVipPortRsParams{} + var ( + // initialize parameters with default values + + snapshotDefault = bool(true) + ) + + return PostVsVipPortRsParams{ + Snapshot: &snapshotDefault, + } } // PostVsVipPortRsParams contains all the bound params for the post vs vip port rs operation @@ -43,6 +51,11 @@ type PostVsVipPortRsParams struct { In: body */ Rss *models.RealServerTinyList + /* + In: query + Default: true + */ + Snapshot *bool } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -54,6 +67,8 @@ func (o *PostVsVipPortRsParams) BindRequest(r *http.Request, route *middleware.M o.HTTPRequest = r + qs := runtime.Values(r.URL.Query()) + rVipPort, rhkVipPort, _ := route.Params.GetOK("VipPort") if err := o.bindVipPort(rVipPort, rhkVipPort, route.Formats); err != nil { res = append(res, err) @@ -80,6 +95,11 @@ func (o *PostVsVipPortRsParams) BindRequest(r *http.Request, route *middleware.M } } } + + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") + if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { + res = append(res, err) + } if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -99,3 +119,27 @@ func (o *PostVsVipPortRsParams) bindVipPort(rawData []string, hasKey bool, forma return nil } + +// bindSnapshot binds and validates parameter Snapshot from query. +func (o *PostVsVipPortRsParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewPostVsVipPortRsParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("snapshot", "query", "bool", raw) + } + o.Snapshot = &value + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/post_vs_vip_port_rs_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/post_vs_vip_port_rs_urlbuilder.go index 1c7f7b01e..c596ec741 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/post_vs_vip_port_rs_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/post_vs_vip_port_rs_urlbuilder.go @@ -10,12 +10,16 @@ import ( "net/url" golangswaggerpaths "path" "strings" + + "github.com/go-openapi/swag" ) // PostVsVipPortRsURL generates an URL for the post vs vip port rs operation type PostVsVipPortRsURL struct { VipPort string + Snapshot *bool + _basePath string // avoid unkeyed usage _ struct{} @@ -55,6 +59,18 @@ func (o *PostVsVipPortRsURL) Build() (*url.URL, error) { } _result.Path = golangswaggerpaths.Join(_basePath, _path) + qs := make(url.Values) + + var snapshotQ string + if o.Snapshot != nil { + snapshotQ = swag.FormatBool(*o.Snapshot) + } + if snapshotQ != "" { + qs.Set("snapshot", snapshotQ) + } + + _result.RawQuery = qs.Encode() + return &_result, nil } diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_laddr_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_laddr_parameters.go index 94c97a03a..a4089848d 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_laddr_parameters.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_laddr_parameters.go @@ -12,17 +12,25 @@ import ( "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/go-openapi/validate" "github.com/dpvs-agent/models" ) // NewPutVsVipPortLaddrParams creates a new PutVsVipPortLaddrParams object -// -// There are no default values defined in the spec. +// with the default values initialized. func NewPutVsVipPortLaddrParams() PutVsVipPortLaddrParams { - return PutVsVipPortLaddrParams{} + var ( + // initialize parameters with default values + + snapshotDefault = bool(true) + ) + + return PutVsVipPortLaddrParams{ + Snapshot: &snapshotDefault, + } } // PutVsVipPortLaddrParams contains all the bound params for the put vs vip port laddr operation @@ -39,6 +47,11 @@ type PutVsVipPortLaddrParams struct { In: path */ VipPort string + /* + In: query + Default: true + */ + Snapshot *bool /* In: body */ @@ -54,11 +67,18 @@ func (o *PutVsVipPortLaddrParams) BindRequest(r *http.Request, route *middleware o.HTTPRequest = r + qs := runtime.Values(r.URL.Query()) + rVipPort, rhkVipPort, _ := route.Params.GetOK("VipPort") if err := o.bindVipPort(rVipPort, rhkVipPort, route.Formats); err != nil { res = append(res, err) } + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") + if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { + res = append(res, err) + } + if runtime.HasBody(r) { defer r.Body.Close() var body models.LocalAddressSpecTiny @@ -99,3 +119,27 @@ func (o *PutVsVipPortLaddrParams) bindVipPort(rawData []string, hasKey bool, for return nil } + +// bindSnapshot binds and validates parameter Snapshot from query. +func (o *PutVsVipPortLaddrParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewPutVsVipPortLaddrParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("snapshot", "query", "bool", raw) + } + o.Snapshot = &value + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_laddr_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_laddr_urlbuilder.go index c0dc2d7d5..34bd5f56b 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_laddr_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_laddr_urlbuilder.go @@ -10,12 +10,16 @@ import ( "net/url" golangswaggerpaths "path" "strings" + + "github.com/go-openapi/swag" ) // PutVsVipPortLaddrURL generates an URL for the put vs vip port laddr operation type PutVsVipPortLaddrURL struct { VipPort string + Snapshot *bool + _basePath string // avoid unkeyed usage _ struct{} @@ -55,6 +59,18 @@ func (o *PutVsVipPortLaddrURL) Build() (*url.URL, error) { } _result.Path = golangswaggerpaths.Join(_basePath, _path) + qs := make(url.Values) + + var snapshotQ string + if o.Snapshot != nil { + snapshotQ = swag.FormatBool(*o.Snapshot) + } + if snapshotQ != "" { + qs.Set("snapshot", snapshotQ) + } + + _result.RawQuery = qs.Encode() + return &_result, nil } diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_parameters.go index 8dc0c2030..4aa52d23a 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_parameters.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_parameters.go @@ -12,17 +12,25 @@ import ( "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/go-openapi/validate" "github.com/dpvs-agent/models" ) // NewPutVsVipPortParams creates a new PutVsVipPortParams object -// -// There are no default values defined in the spec. +// with the default values initialized. func NewPutVsVipPortParams() PutVsVipPortParams { - return PutVsVipPortParams{} + var ( + // initialize parameters with default values + + snapshotDefault = bool(true) + ) + + return PutVsVipPortParams{ + Snapshot: &snapshotDefault, + } } // PutVsVipPortParams contains all the bound params for the put vs vip port operation @@ -39,6 +47,11 @@ type PutVsVipPortParams struct { In: path */ VipPort string + /* + In: query + Default: true + */ + Snapshot *bool /* In: body */ @@ -54,11 +67,18 @@ func (o *PutVsVipPortParams) BindRequest(r *http.Request, route *middleware.Matc o.HTTPRequest = r + qs := runtime.Values(r.URL.Query()) + rVipPort, rhkVipPort, _ := route.Params.GetOK("VipPort") if err := o.bindVipPort(rVipPort, rhkVipPort, route.Formats); err != nil { res = append(res, err) } + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") + if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { + res = append(res, err) + } + if runtime.HasBody(r) { defer r.Body.Close() var body models.VirtualServerSpecTiny @@ -99,3 +119,27 @@ func (o *PutVsVipPortParams) bindVipPort(rawData []string, hasKey bool, formats return nil } + +// bindSnapshot binds and validates parameter Snapshot from query. +func (o *PutVsVipPortParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewPutVsVipPortParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("snapshot", "query", "bool", raw) + } + o.Snapshot = &value + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health.go new file mode 100644 index 000000000..18770d342 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package virtualserver + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// PutVsVipPortRsHealthHandlerFunc turns a function with the right signature into a put vs vip port rs health handler +type PutVsVipPortRsHealthHandlerFunc func(PutVsVipPortRsHealthParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn PutVsVipPortRsHealthHandlerFunc) Handle(params PutVsVipPortRsHealthParams) middleware.Responder { + return fn(params) +} + +// PutVsVipPortRsHealthHandler interface for that can handle valid put vs vip port rs health params +type PutVsVipPortRsHealthHandler interface { + Handle(PutVsVipPortRsHealthParams) middleware.Responder +} + +// NewPutVsVipPortRsHealth creates a new http.Handler for the put vs vip port rs health operation +func NewPutVsVipPortRsHealth(ctx *middleware.Context, handler PutVsVipPortRsHealthHandler) *PutVsVipPortRsHealth { + return &PutVsVipPortRsHealth{Context: ctx, Handler: handler} +} + +/* + PutVsVipPortRsHealth swagger:route PUT /vs/{VipPort}/rs/health virtualserver putVsVipPortRsHealth + +dpvs healthcheck update rs weight +*/ +type PutVsVipPortRsHealth struct { + Context *middleware.Context + Handler PutVsVipPortRsHealthHandler +} + +func (o *PutVsVipPortRsHealth) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewPutVsVipPortRsHealthParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_parameters.go new file mode 100644 index 000000000..ca2d0f9fd --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_parameters.go @@ -0,0 +1,134 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package virtualserver + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + + "github.com/dpvs-agent/models" +) + +// NewPutVsVipPortRsHealthParams creates a new PutVsVipPortRsHealthParams object +// +// There are no default values defined in the spec. +func NewPutVsVipPortRsHealthParams() PutVsVipPortRsHealthParams { + + return PutVsVipPortRsHealthParams{} +} + +// PutVsVipPortRsHealthParams contains all the bound params for the put vs vip port rs health operation +// typically these are obtained from a http.Request +// +// swagger:parameters PutVsVipPortRsHealth +type PutVsVipPortRsHealthParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: path + */ + VipPort string + /* + In: body + */ + Rss *models.RealServerTinyList + /* + Required: true + In: query + */ + Version string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewPutVsVipPortRsHealthParams() beforehand. +func (o *PutVsVipPortRsHealthParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + qs := runtime.Values(r.URL.Query()) + + rVipPort, rhkVipPort, _ := route.Params.GetOK("VipPort") + if err := o.bindVipPort(rVipPort, rhkVipPort, route.Formats); err != nil { + res = append(res, err) + } + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.RealServerTinyList + if err := route.Consumer.Consume(r.Body, &body); err != nil { + res = append(res, errors.NewParseError("rss", "body", "", err)) + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.Rss = &body + } + } + } + + qVersion, qhkVersion, _ := qs.GetOK("version") + if err := o.bindVersion(qVersion, qhkVersion, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindVipPort binds and validates parameter VipPort from path. +func (o *PutVsVipPortRsHealthParams) bindVipPort(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.VipPort = raw + + return nil +} + +// bindVersion binds and validates parameter Version from query. +func (o *PutVsVipPortRsHealthParams) bindVersion(rawData []string, hasKey bool, formats strfmt.Registry) error { + if !hasKey { + return errors.Required("version", "query", rawData) + } + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // AllowEmptyValue: false + + if err := validate.RequiredString("version", "query", raw); err != nil { + return err + } + o.Version = raw + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_responses.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_responses.go new file mode 100644 index 000000000..c3904ebe9 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_responses.go @@ -0,0 +1,233 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package virtualserver + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/dpvs-agent/models" +) + +// PutVsVipPortRsHealthOKCode is the HTTP code returned for type PutVsVipPortRsHealthOK +const PutVsVipPortRsHealthOKCode int = 200 + +/* +PutVsVipPortRsHealthOK Success + +swagger:response putVsVipPortRsHealthOK +*/ +type PutVsVipPortRsHealthOK struct { + + /* + In: Body + */ + Payload *models.RealServerExpandList `json:"body,omitempty"` +} + +// NewPutVsVipPortRsHealthOK creates PutVsVipPortRsHealthOK with default headers values +func NewPutVsVipPortRsHealthOK() *PutVsVipPortRsHealthOK { + + return &PutVsVipPortRsHealthOK{} +} + +// WithPayload adds the payload to the put vs vip port rs health o k response +func (o *PutVsVipPortRsHealthOK) WithPayload(payload *models.RealServerExpandList) *PutVsVipPortRsHealthOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the put vs vip port rs health o k response +func (o *PutVsVipPortRsHealthOK) SetPayload(payload *models.RealServerExpandList) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *PutVsVipPortRsHealthOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// PutVsVipPortRsHealthUnexpectedCode is the HTTP code returned for type PutVsVipPortRsHealthUnexpected +const PutVsVipPortRsHealthUnexpectedCode int = 270 + +/* +PutVsVipPortRsHealthUnexpected the rss-config parameter is outdated, update nothing and return the latest rs info + +swagger:response putVsVipPortRsHealthUnexpected +*/ +type PutVsVipPortRsHealthUnexpected struct { + + /* + In: Body + */ + Payload *models.RealServerExpandList `json:"body,omitempty"` +} + +// NewPutVsVipPortRsHealthUnexpected creates PutVsVipPortRsHealthUnexpected with default headers values +func NewPutVsVipPortRsHealthUnexpected() *PutVsVipPortRsHealthUnexpected { + + return &PutVsVipPortRsHealthUnexpected{} +} + +// WithPayload adds the payload to the put vs vip port rs health unexpected response +func (o *PutVsVipPortRsHealthUnexpected) WithPayload(payload *models.RealServerExpandList) *PutVsVipPortRsHealthUnexpected { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the put vs vip port rs health unexpected response +func (o *PutVsVipPortRsHealthUnexpected) SetPayload(payload *models.RealServerExpandList) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *PutVsVipPortRsHealthUnexpected) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(270) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// PutVsVipPortRsHealthInvalidFrontendCode is the HTTP code returned for type PutVsVipPortRsHealthInvalidFrontend +const PutVsVipPortRsHealthInvalidFrontendCode int = 460 + +/* +PutVsVipPortRsHealthInvalidFrontend Invalid frontend in service configuration + +swagger:response putVsVipPortRsHealthInvalidFrontend +*/ +type PutVsVipPortRsHealthInvalidFrontend struct { + + /* + In: Body + */ + Payload models.Error `json:"body,omitempty"` +} + +// NewPutVsVipPortRsHealthInvalidFrontend creates PutVsVipPortRsHealthInvalidFrontend with default headers values +func NewPutVsVipPortRsHealthInvalidFrontend() *PutVsVipPortRsHealthInvalidFrontend { + + return &PutVsVipPortRsHealthInvalidFrontend{} +} + +// WithPayload adds the payload to the put vs vip port rs health invalid frontend response +func (o *PutVsVipPortRsHealthInvalidFrontend) WithPayload(payload models.Error) *PutVsVipPortRsHealthInvalidFrontend { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the put vs vip port rs health invalid frontend response +func (o *PutVsVipPortRsHealthInvalidFrontend) SetPayload(payload models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *PutVsVipPortRsHealthInvalidFrontend) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(460) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// PutVsVipPortRsHealthInvalidBackendCode is the HTTP code returned for type PutVsVipPortRsHealthInvalidBackend +const PutVsVipPortRsHealthInvalidBackendCode int = 461 + +/* +PutVsVipPortRsHealthInvalidBackend Invalid backend in service configuration + +swagger:response putVsVipPortRsHealthInvalidBackend +*/ +type PutVsVipPortRsHealthInvalidBackend struct { + + /* + In: Body + */ + Payload models.Error `json:"body,omitempty"` +} + +// NewPutVsVipPortRsHealthInvalidBackend creates PutVsVipPortRsHealthInvalidBackend with default headers values +func NewPutVsVipPortRsHealthInvalidBackend() *PutVsVipPortRsHealthInvalidBackend { + + return &PutVsVipPortRsHealthInvalidBackend{} +} + +// WithPayload adds the payload to the put vs vip port rs health invalid backend response +func (o *PutVsVipPortRsHealthInvalidBackend) WithPayload(payload models.Error) *PutVsVipPortRsHealthInvalidBackend { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the put vs vip port rs health invalid backend response +func (o *PutVsVipPortRsHealthInvalidBackend) SetPayload(payload models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *PutVsVipPortRsHealthInvalidBackend) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(461) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// PutVsVipPortRsHealthFailureCode is the HTTP code returned for type PutVsVipPortRsHealthFailure +const PutVsVipPortRsHealthFailureCode int = 500 + +/* +PutVsVipPortRsHealthFailure Service deletion failed + +swagger:response putVsVipPortRsHealthFailure +*/ +type PutVsVipPortRsHealthFailure struct { + + /* + In: Body + */ + Payload models.Error `json:"body,omitempty"` +} + +// NewPutVsVipPortRsHealthFailure creates PutVsVipPortRsHealthFailure with default headers values +func NewPutVsVipPortRsHealthFailure() *PutVsVipPortRsHealthFailure { + + return &PutVsVipPortRsHealthFailure{} +} + +// WithPayload adds the payload to the put vs vip port rs health failure response +func (o *PutVsVipPortRsHealthFailure) WithPayload(payload models.Error) *PutVsVipPortRsHealthFailure { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the put vs vip port rs health failure response +func (o *PutVsVipPortRsHealthFailure) SetPayload(payload models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *PutVsVipPortRsHealthFailure) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_urlbuilder.go new file mode 100644 index 000000000..9ae7a46a7 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_urlbuilder.go @@ -0,0 +1,110 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package virtualserver + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// PutVsVipPortRsHealthURL generates an URL for the put vs vip port rs health operation +type PutVsVipPortRsHealthURL struct { + VipPort string + + Version string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *PutVsVipPortRsHealthURL) WithBasePath(bp string) *PutVsVipPortRsHealthURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *PutVsVipPortRsHealthURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *PutVsVipPortRsHealthURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/vs/{VipPort}/rs/health" + + vipPort := o.VipPort + if vipPort != "" { + _path = strings.Replace(_path, "{VipPort}", vipPort, -1) + } else { + return nil, errors.New("vipPort is required on PutVsVipPortRsHealthURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/v2" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + qs := make(url.Values) + + versionQ := o.Version + if versionQ != "" { + qs.Set("version", versionQ) + } + + _result.RawQuery = qs.Encode() + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *PutVsVipPortRsHealthURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *PutVsVipPortRsHealthURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *PutVsVipPortRsHealthURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on PutVsVipPortRsHealthURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on PutVsVipPortRsHealthURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *PutVsVipPortRsHealthURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_parameters.go index 0e36def24..6a0bc5188 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_parameters.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_parameters.go @@ -12,25 +12,17 @@ import ( "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" - "github.com/go-openapi/swag" "github.com/go-openapi/validate" "github.com/dpvs-agent/models" ) // NewPutVsVipPortRsParams creates a new PutVsVipPortRsParams object -// with the default values initialized. +// +// There are no default values defined in the spec. func NewPutVsVipPortRsParams() PutVsVipPortRsParams { - var ( - // initialize parameters with default values - - healthcheckDefault = bool(false) - ) - - return PutVsVipPortRsParams{ - Healthcheck: &healthcheckDefault, - } + return PutVsVipPortRsParams{} } // PutVsVipPortRsParams contains all the bound params for the put vs vip port rs operation @@ -47,11 +39,6 @@ type PutVsVipPortRsParams struct { In: path */ VipPort string - /* - In: query - Default: false - */ - Healthcheck *bool /* In: body */ @@ -67,18 +54,11 @@ func (o *PutVsVipPortRsParams) BindRequest(r *http.Request, route *middleware.Ma o.HTTPRequest = r - qs := runtime.Values(r.URL.Query()) - rVipPort, rhkVipPort, _ := route.Params.GetOK("VipPort") if err := o.bindVipPort(rVipPort, rhkVipPort, route.Formats); err != nil { res = append(res, err) } - qHealthcheck, qhkHealthcheck, _ := qs.GetOK("healthcheck") - if err := o.bindHealthcheck(qHealthcheck, qhkHealthcheck, route.Formats); err != nil { - res = append(res, err) - } - if runtime.HasBody(r) { defer r.Body.Close() var body models.RealServerTinyList @@ -119,27 +99,3 @@ func (o *PutVsVipPortRsParams) bindVipPort(rawData []string, hasKey bool, format return nil } - -// bindHealthcheck binds and validates parameter Healthcheck from query. -func (o *PutVsVipPortRsParams) bindHealthcheck(rawData []string, hasKey bool, formats strfmt.Registry) error { - var raw string - if len(rawData) > 0 { - raw = rawData[len(rawData)-1] - } - - // Required: false - // AllowEmptyValue: false - - if raw == "" { // empty values pass all other validations - // Default values have been previously initialized by NewPutVsVipPortRsParams() - return nil - } - - value, err := swag.ConvertBool(raw) - if err != nil { - return errors.InvalidType("healthcheck", "query", "bool", raw) - } - o.Healthcheck = &value - - return nil -} diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_urlbuilder.go index d906fae36..90b4750e4 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_urlbuilder.go @@ -10,16 +10,12 @@ import ( "net/url" golangswaggerpaths "path" "strings" - - "github.com/go-openapi/swag" ) // PutVsVipPortRsURL generates an URL for the put vs vip port rs operation type PutVsVipPortRsURL struct { VipPort string - Healthcheck *bool - _basePath string // avoid unkeyed usage _ struct{} @@ -59,18 +55,6 @@ func (o *PutVsVipPortRsURL) Build() (*url.URL, error) { } _result.Path = golangswaggerpaths.Join(_basePath, _path) - qs := make(url.Values) - - var healthcheckQ string - if o.Healthcheck != nil { - healthcheckQ = swag.FormatBool(*o.Healthcheck) - } - if healthcheckQ != "" { - qs.Set("healthcheck", healthcheckQ) - } - - _result.RawQuery = qs.Encode() - return &_result, nil } diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_urlbuilder.go index 01adfb16c..a88b8ca67 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_urlbuilder.go @@ -10,12 +10,16 @@ import ( "net/url" golangswaggerpaths "path" "strings" + + "github.com/go-openapi/swag" ) // PutVsVipPortURL generates an URL for the put vs vip port operation type PutVsVipPortURL struct { VipPort string + Snapshot *bool + _basePath string // avoid unkeyed usage _ struct{} @@ -55,6 +59,18 @@ func (o *PutVsVipPortURL) Build() (*url.URL, error) { } _result.Path = golangswaggerpaths.Join(_basePath, _path) + qs := make(url.Values) + + var snapshotQ string + if o.Snapshot != nil { + snapshotQ = swag.FormatBool(*o.Snapshot) + } + if snapshotQ != "" { + qs.Set("snapshot", snapshotQ) + } + + _result.RawQuery = qs.Encode() + return &_result, nil } From 7262f4bc223a7d9993635e7214f224d87f75c2f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 03:26:32 +0000 Subject: [PATCH 02/63] build(deps): bump golang.org/x/net in /tools/dpvs-agent Bumps [golang.org/x/net](https://github.com/golang/net) from 0.10.0 to 0.17.0. - [Commits](https://github.com/golang/net/compare/v0.10.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- tools/dpvs-agent/go.mod | 4 ++-- tools/dpvs-agent/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/dpvs-agent/go.mod b/tools/dpvs-agent/go.mod index b5198bfc6..2ca083f5e 100644 --- a/tools/dpvs-agent/go.mod +++ b/tools/dpvs-agent/go.mod @@ -14,8 +14,8 @@ require ( github.com/jessevdk/go-flags v1.5.0 github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/vishvananda/netlink v1.1.0 - golang.org/x/net v0.10.0 - golang.org/x/sys v0.8.0 + golang.org/x/net v0.17.0 + golang.org/x/sys v0.13.0 ) require ( diff --git a/tools/dpvs-agent/go.sum b/tools/dpvs-agent/go.sum index f6f0a32b6..72379d46b 100644 --- a/tools/dpvs-agent/go.sum +++ b/tools/dpvs-agent/go.sum @@ -177,8 +177,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -202,8 +202,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From 9648beba2403ffa29478c30b589073d807298fce Mon Sep 17 00:00:00 2001 From: huangyichen Date: Thu, 18 Jan 2024 18:13:58 +0800 Subject: [PATCH 03/63] API: /${SVCID}/rs/health return the specified service detail when fetched wrong vs-version from client --- .../cmd/ipvs/put_vs_vip_port_rs_health.go | 14 ++++----- tools/dpvs-agent/dpvs-agent-api.yaml | 7 +++-- tools/dpvs-agent/pkg/ipc/types/snapshot.go | 7 +++++ tools/dpvs-agent/restapi/embedded_spec.go | 14 +++------ .../put_vs_vip_port_rs_health_responses.go | 30 ++++--------------- 5 files changed, 26 insertions(+), 46 deletions(-) diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go index 31d56449f..ad9701ee1 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go @@ -21,7 +21,6 @@ import ( "github.com/dpvs-agent/pkg/ipc/types" "github.com/dpvs-agent/pkg/settings" - "github.com/dpvs-agent/models" apiVs "github.com/dpvs-agent/restapi/operations/virtualserver" "github.com/go-openapi/runtime/middleware" @@ -62,11 +61,9 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa activeRSs[rs.ID()] = rs } - rssModels := new(models.RealServerExpandList) - rssModels.Items = make([]*models.RealServerSpecExpand, len(active)) validRSs := make([]*types.RealServerSpec, 0) if params.Rss != nil { - for i, rs := range params.Rss.Items { + for _, rs := range params.Rss.Items { var fwdmode types.DpvsFwdMode fwdmode.FromString(rs.Mode) newRs := types.NewRealServerSpec() @@ -79,16 +76,17 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa newRs.SetInhibited(rs.Inhibited) newRs.SetOverloaded(rs.Overloaded) - if activeRs, existed := activeRSs[newRs.ID()]; existed { - rssModels.Items[i] = activeRs.GetModel() + if _, existed := activeRSs[newRs.ID()]; existed { validRSs = append(validRSs, newRs) } } } + vsModel := shareSnapshot.ServiceGet(params.VipPort) + if !strings.EqualFold(params.Version, version) { h.logger.Info("The service", "VipPort", params.VipPort, "version expired. The newest version", version) - return apiVs.NewPutVsVipPortRsHealthUnexpected().WithPayload(rssModels) + return apiVs.NewPutVsVipPortRsHealthUnexpected().WithPayload(vsModel) } existOnly := true @@ -96,7 +94,7 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa switch result { case types.EDPVS_EXIST, types.EDPVS_OK: h.logger.Info("Set real server sets success.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) - return apiVs.NewPutVsVipPortRsHealthOK().WithPayload(rssModels) + return apiVs.NewPutVsVipPortRsHealthOK() case types.EDPVS_NOTEXIST: if existOnly { h.logger.Error("Edit not exist real server.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) diff --git a/tools/dpvs-agent/dpvs-agent-api.yaml b/tools/dpvs-agent/dpvs-agent-api.yaml index 0640a145c..799c956c9 100644 --- a/tools/dpvs-agent/dpvs-agent-api.yaml +++ b/tools/dpvs-agent/dpvs-agent-api.yaml @@ -1110,13 +1110,14 @@ paths: responses: '200': description: Success - schema: - "$ref": "#/definitions/RealServerExpandList" + #schema: + # "$ref": "#/definitions/RealServerExpandList" '270': description: "the rss-config parameter is outdated, update nothing and return the latest rs info" x-go-name: Unexpected schema: - "$ref": "#/definitions/RealServerExpandList" + # "$ref": "#/definitions/RealServerExpandList" + "$ref": "#/definitions/VirtualServerSpecExpand" '460': description: Invalid frontend in service configuration x-go-name: InvalidFrontend diff --git a/tools/dpvs-agent/pkg/ipc/types/snapshot.go b/tools/dpvs-agent/pkg/ipc/types/snapshot.go index 4491d82c9..aea1fed22 100644 --- a/tools/dpvs-agent/pkg/ipc/types/snapshot.go +++ b/tools/dpvs-agent/pkg/ipc/types/snapshot.go @@ -29,6 +29,13 @@ func (snapshot *NodeSnapshot) ServiceVersionUpdate(id string, logger hclog.Logge logger.Error("Update service version failed.", "id", id) } +func (snapshot *NodeSnapshot) ServiceGet(id string) *models.VirtualServerSpecExpand { + if svc, exist := snapshot.Services[strings.ToLower(id)]; exist { + return svc + } + return nil +} + func (snapshot *NodeSnapshot) ServiceDel(id string) { if _, exist := snapshot.Services[strings.ToLower(id)]; exist { delete(snapshot.Services, strings.ToLower(id)) diff --git a/tools/dpvs-agent/restapi/embedded_spec.go b/tools/dpvs-agent/restapi/embedded_spec.go index f55b028fe..13baf4079 100644 --- a/tools/dpvs-agent/restapi/embedded_spec.go +++ b/tools/dpvs-agent/restapi/embedded_spec.go @@ -1370,15 +1370,12 @@ func init() { ], "responses": { "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/RealServerExpandList" - } + "description": "Success" }, "270": { "description": "the rss-config parameter is outdated, update nothing and return the latest rs info", "schema": { - "$ref": "#/definitions/RealServerExpandList" + "$ref": "#/definitions/VirtualServerSpecExpand" }, "x-go-name": "Unexpected" }, @@ -3894,15 +3891,12 @@ func init() { ], "responses": { "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/RealServerExpandList" - } + "description": "Success" }, "270": { "description": "the rss-config parameter is outdated, update nothing and return the latest rs info", "schema": { - "$ref": "#/definitions/RealServerExpandList" + "$ref": "#/definitions/VirtualServerSpecExpand" }, "x-go-name": "Unexpected" }, diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_responses.go b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_responses.go index c3904ebe9..b00b1d3fa 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_responses.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/put_vs_vip_port_rs_health_responses.go @@ -22,11 +22,6 @@ PutVsVipPortRsHealthOK Success swagger:response putVsVipPortRsHealthOK */ type PutVsVipPortRsHealthOK struct { - - /* - In: Body - */ - Payload *models.RealServerExpandList `json:"body,omitempty"` } // NewPutVsVipPortRsHealthOK creates PutVsVipPortRsHealthOK with default headers values @@ -35,27 +30,12 @@ func NewPutVsVipPortRsHealthOK() *PutVsVipPortRsHealthOK { return &PutVsVipPortRsHealthOK{} } -// WithPayload adds the payload to the put vs vip port rs health o k response -func (o *PutVsVipPortRsHealthOK) WithPayload(payload *models.RealServerExpandList) *PutVsVipPortRsHealthOK { - o.Payload = payload - return o -} - -// SetPayload sets the payload to the put vs vip port rs health o k response -func (o *PutVsVipPortRsHealthOK) SetPayload(payload *models.RealServerExpandList) { - o.Payload = payload -} - // WriteResponse to the client func (o *PutVsVipPortRsHealthOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + rw.WriteHeader(200) - if o.Payload != nil { - payload := o.Payload - if err := producer.Produce(rw, payload); err != nil { - panic(err) // let the recovery middleware deal with this - } - } } // PutVsVipPortRsHealthUnexpectedCode is the HTTP code returned for type PutVsVipPortRsHealthUnexpected @@ -71,7 +51,7 @@ type PutVsVipPortRsHealthUnexpected struct { /* In: Body */ - Payload *models.RealServerExpandList `json:"body,omitempty"` + Payload *models.VirtualServerSpecExpand `json:"body,omitempty"` } // NewPutVsVipPortRsHealthUnexpected creates PutVsVipPortRsHealthUnexpected with default headers values @@ -81,13 +61,13 @@ func NewPutVsVipPortRsHealthUnexpected() *PutVsVipPortRsHealthUnexpected { } // WithPayload adds the payload to the put vs vip port rs health unexpected response -func (o *PutVsVipPortRsHealthUnexpected) WithPayload(payload *models.RealServerExpandList) *PutVsVipPortRsHealthUnexpected { +func (o *PutVsVipPortRsHealthUnexpected) WithPayload(payload *models.VirtualServerSpecExpand) *PutVsVipPortRsHealthUnexpected { o.Payload = payload return o } // SetPayload sets the payload to the put vs vip port rs health unexpected response -func (o *PutVsVipPortRsHealthUnexpected) SetPayload(payload *models.RealServerExpandList) { +func (o *PutVsVipPortRsHealthUnexpected) SetPayload(payload *models.VirtualServerSpecExpand) { o.Payload = payload } From f9ab755fa8ab3acc33ea5e8811435ffac56958a3 Mon Sep 17 00:00:00 2001 From: huangyichen Date: Mon, 22 Jan 2024 20:12:07 +0800 Subject: [PATCH 04/63] lock snapshot --- .../cmd/dpvs-agent-server/local_init.go | 11 +- .../dpvs-agent/cmd/ipvs/delete_vs_vip_port.go | 11 +- tools/dpvs-agent/cmd/ipvs/get_vs.go | 2 + tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go | 7 +- .../cmd/ipvs/post_vs_vip_port_rs.go | 24 +++- tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go | 31 ++++- .../dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go | 22 +++ .../cmd/ipvs/put_vs_vip_port_rs_health.go | 95 ++++++++++++- tools/dpvs-agent/pkg/ipc/types/snapshot.go | 130 +++++++++++++----- tools/dpvs-agent/pkg/settings/settings.go | 2 +- 10 files changed, 283 insertions(+), 52 deletions(-) diff --git a/tools/dpvs-agent/cmd/dpvs-agent-server/local_init.go b/tools/dpvs-agent/cmd/dpvs-agent-server/local_init.go index f5a8a6968..b8bb050e5 100644 --- a/tools/dpvs-agent/cmd/dpvs-agent-server/local_init.go +++ b/tools/dpvs-agent/cmd/dpvs-agent-server/local_init.go @@ -32,15 +32,16 @@ func (agent *DpvsAgentServer) LocalLoad(cp *pool.ConnPool, parentLogger hclog.Lo logger = parentLogger.Named("LoadConfigFile") } - snapshot := settings.ShareSnapshot() - if err := snapshot.LoadFrom(settings.LocalConfigFile(), logger); err != nil { + nodeSnap := settings.ShareSnapshot() + if err := nodeSnap.LoadFrom(settings.LocalConfigFile(), logger); err != nil { return err } - announcePort := snapshot.NodeSpec.AnnouncePort - laddrs := snapshot.NodeSpec.Laddrs + announcePort := nodeSnap.NodeSpec.AnnouncePort + laddrs := nodeSnap.NodeSpec.Laddrs - for _, service := range snapshot.Services { + for _, snap := range nodeSnap.Snapshot { + service := snap.Service // 1> ipvsadm -A vip:port -s wrr vs := types.NewVirtualServerSpec() vs.SetAddr(service.Addr) diff --git a/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port.go b/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port.go index 03a0f7a6c..9e0429e08 100644 --- a/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port.go +++ b/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port.go @@ -45,14 +45,21 @@ func (h *delVsItem) Handle(params apiVs.DeleteVsVipPortParams) middleware.Respon return apiVs.NewDeleteVsVipPortFailure() } + shareSnapshot := settings.ShareSnapshot() + snapshot := shareSnapshot.SnapshotGet(params.VipPort) + if snapshot != nil { + snapshot.Lock() + defer snapshot.Unlock() + } + result := vs.Del(h.connPool, h.logger) switch result { case types.EDPVS_OK: - settings.ShareSnapshot().ServiceDel(params.VipPort) + shareSnapshot.ServiceDel(params.VipPort) h.logger.Info("Del virtual server success.", "VipPort", params.VipPort) return apiVs.NewDeleteVsVipPortOK() case types.EDPVS_NOTEXIST: - settings.ShareSnapshot().ServiceDel(params.VipPort) + shareSnapshot.ServiceDel(params.VipPort) h.logger.Warn("Del a not exist virtual server done.", "VipPort", params.VipPort, "result", result.String()) return apiVs.NewDeleteVsVipPortNotFound() default: diff --git a/tools/dpvs-agent/cmd/ipvs/get_vs.go b/tools/dpvs-agent/cmd/ipvs/get_vs.go index d9cd5b798..4a366544b 100644 --- a/tools/dpvs-agent/cmd/ipvs/get_vs.go +++ b/tools/dpvs-agent/cmd/ipvs/get_vs.go @@ -102,7 +102,9 @@ func (h *getVs) Handle(params apiVs.GetVsParams) middleware.Responder { } } + shareSnapshot.ServiceLock(vs.ID()) shareSnapshot.ServiceUpsert(vsModel) + shareSnapshot.ServiceUnlock(vs.ID()) } if params.Snapshot != nil && *params.Snapshot { diff --git a/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go b/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go index 91c7bc60c..a05cb3f0b 100644 --- a/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go +++ b/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go @@ -80,8 +80,7 @@ func (h *getVsVipPort) Handle(params apiVs.GetVsVipPortParams) middleware.Respon h.logger.Info("Get real server list of virtual server success.", "ID", vs.ID(), "rss", rss) vsModel := vs.GetModel() - shareSnapshot.ServiceUpsert(vsModel) - // vsModel.Version = shareSnapshot.ServiceVersion(vs.ID()) + vsModels.Items[i] = vsModel vsStats := (*types.ServerStats)(vsModels.Items[i].Stats) vsModels.Items[i].RSs = new(models.RealServerExpandList) @@ -93,6 +92,10 @@ func (h *getVsVipPort) Handle(params apiVs.GetVsVipPortParams) middleware.Respon vsModels.Items[i].RSs.Items[j] = rsModel vsStats.Increase(rsStats) } + + shareSnapshot.ServiceLock(vs.ID()) + shareSnapshot.ServiceUpsert(vsModel) + shareSnapshot.ServiceUnlock(vs.ID()) } return apiVs.NewGetVsVipPortOK().WithPayload(vsModels) diff --git a/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go index aa4c98506..8665668a1 100644 --- a/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go @@ -15,7 +15,7 @@ package ipvs import ( - // "github.com/dpvs-agent/models" + "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" "github.com/dpvs-agent/pkg/settings" @@ -61,10 +61,30 @@ func (h *postVsRs) Handle(params apiVs.PostVsVipPortRsParams) middleware.Respond rss[i].SetFwdMode(fwdmode) } + shareSnapshot := settings.ShareSnapshot() + if shareSnapshot.ServiceLock(params.VipPort) { + defer shareSnapshot.ServiceUnlock(params.VipPort) + } + result := front.Update(rss, h.connPool, h.logger) switch result { case types.EDPVS_EXIST, types.EDPVS_OK: - settings.ShareSnapshot().ServiceVersionUpdate(params.VipPort, h.logger) + // Update Snapshot + if newRSs, err := front.Get(h.connPool, h.logger); err == nil { + rsModels := new(models.RealServerExpandList) + rsModels.Items = make([]*models.RealServerSpecExpand, len(newRSs)) + for i, rs := range newRSs { + rsModels.Items[i] = rs.GetModel() + } + + vsModel := shareSnapshot.ServiceGet(params.VipPort) + if vsModel != nil { + vsModel.RSs = rsModels + shareSnapshot.ServiceUpsert(vsModel) + } + } + shareSnapshot.ServiceVersionUpdate(params.VipPort, h.logger) + h.logger.Info("Set real server to virtual server success.", "VipPort", params.VipPort, "rss", rss, "result", result.String()) return apiVs.NewPostVsVipPortRsOK() default: diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go index 0ee30e708..57f175c55 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go @@ -17,6 +17,7 @@ package ipvs import ( "strings" + "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" "github.com/dpvs-agent/pkg/settings" @@ -85,16 +86,21 @@ func (h *putVsItem) Handle(params apiVs.PutVsVipPortParams) middleware.Responder } } + shareSnapshot := settings.ShareSnapshot() result := vs.Add(h.connPool, h.logger) h.logger.Info("Add virtual server done.", "vs", vs, "result", result.String()) switch result { case types.EDPVS_OK: // return 201 - settings.ShareSnapshot().ServiceAdd(vs) + shareSnapshot.ServiceAdd(vs) h.logger.Info("Created new virtual server success.", "VipPort", params.VipPort) return apiVs.NewPutVsVipPortCreated() case types.EDPVS_EXIST: h.logger.Info("The virtual server already exist! Try to update.", "VipPort", params.VipPort) + if shareSnapshot.ServiceLock(vs.ID()) { + defer shareSnapshot.ServiceUnlock(vs.ID()) + } + reason := vs.Update(h.connPool, h.logger) if reason != types.EDPVS_OK { // return 461 @@ -102,6 +108,29 @@ func (h *putVsItem) Handle(params apiVs.PutVsVipPortParams) middleware.Responder return apiVs.NewPutVsVipPortInvalidBackend() } h.logger.Info("Update virtual server success.", "VipPort", params.VipPort) + + if vss, err := vs.Get(h.connPool, h.logger); err == nil { + for _, newVs := range vss { + front := types.NewRealServerFront() + if err := front.ParseVipPortProto(newVs.ID()); err != nil { + continue + } + + vsModel := newVs.GetModel() + front.SetNumDests(newVs.GetNumDests()) + if rss, err := front.Get(h.connPool, h.logger); err != nil { + vsModel.RSs = new(models.RealServerExpandList) + vsModel.RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) + for i, rs := range rss { + vsModel.RSs.Items[i] = rs.GetModel() + } + } + + shareSnapshot.ServiceLock(newVs.ID()) + shareSnapshot.ServiceUpsert(vsModel) + shareSnapshot.ServiceUnlock(newVs.ID()) + } + } // return 200 return apiVs.NewPutVsVipPortOK() default: diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go index 2c596ab27..5f68ce323 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go @@ -15,8 +15,10 @@ package ipvs import ( + "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" + "github.com/dpvs-agent/pkg/settings" apiVs "github.com/dpvs-agent/restapi/operations/virtualserver" @@ -62,6 +64,11 @@ func (h *putVsRs) Handle(params apiVs.PutVsVipPortRsParams) middleware.Responder } } + shareSnapshot := settings.ShareSnapshot() + if shareSnapshot.ServiceLock(params.VipPort) { + defer shareSnapshot.ServiceUnlock(params.VipPort) + } + existOnly := false result := front.Edit(existOnly, rss, h.connPool, h.logger) @@ -69,6 +76,21 @@ func (h *putVsRs) Handle(params apiVs.PutVsVipPortRsParams) middleware.Responder switch result { case types.EDPVS_EXIST, types.EDPVS_OK: h.logger.Info("Set real server sets success.", "VipPort", params.VipPort, "rss", rss, "result", result.String()) + // Update Snapshot + if newRSs, err := front.Get(h.connPool, h.logger); err == nil { + rsModels := new(models.RealServerExpandList) + rsModels.Items = make([]*models.RealServerSpecExpand, len(newRSs)) + for i, rs := range newRSs { + rsModels.Items[i] = rs.GetModel() + } + + vsModel := shareSnapshot.ServiceGet(params.VipPort) + if vsModel != nil { + vsModel.RSs = rsModels + shareSnapshot.ServiceUpsert(vsModel) + } + } + shareSnapshot.ServiceVersionUpdate(params.VipPort, h.logger) return apiVs.NewPutVsVipPortRsOK() case types.EDPVS_NOTEXIST: h.logger.Error("Unreachable branch") diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go index ad9701ee1..d36e66f66 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go @@ -17,6 +17,7 @@ package ipvs import ( "strings" + "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" "github.com/dpvs-agent/pkg/settings" @@ -47,14 +48,17 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa return apiVs.NewPutVsVipPortRsHealthInvalidFrontend() } + shareSnapshot := settings.ShareSnapshot() + + shareSnapshot.ServiceRLock(params.VipPort) // RLock + version := shareSnapshot.ServiceVersion(params.VipPort) // get active backends active, err := front.Get(h.connPool, h.logger) if err != nil { + shareSnapshot.ServiceRUnlock(params.VipPort) // RUnlock return apiVs.NewPutVsVipPortRsHealthInvalidBackend() } - - shareSnapshot := settings.ShareSnapshot() - version := shareSnapshot.ServiceVersion(params.VipPort) + shareSnapshot.ServiceRUnlock(params.VipPort) // RUnlock activeRSs := make(map[string]*types.RealServerSpec) for _, rs := range active { @@ -82,17 +86,98 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa } } - vsModel := shareSnapshot.ServiceGet(params.VipPort) - if !strings.EqualFold(params.Version, version) { h.logger.Info("The service", "VipPort", params.VipPort, "version expired. The newest version", version) + if shareSnapshot.ServiceRLock(params.VipPort) { + defer shareSnapshot.ServiceRUnlock(params.VipPort) + } + vsModel := shareSnapshot.ServiceGet(params.VipPort) + if vsModel == nil { + spec := types.NewVirtualServerSpec() + spec.ParseVipPortProto(params.VipPort) + + vss, err := spec.Get(h.connPool, h.logger) + if err != nil { + return apiVs.NewPutVsVipPortRsHealthInvalidBackend() + } + for _, vs := range vss { + front := types.NewRealServerFront() + front.ParseVipPortProto(vs.ID()) + + front.SetNumDests(vs.GetNumDests()) + + rss, err := front.Get(h.connPool, h.logger) + if err != nil { + h.logger.Error("Get real server list of virtual server failed.", "ID", vs.ID(), "Error", err.Error()) + return apiVs.NewPutVsVipPortRsHealthInvalidBackend() + } + vsModel = vs.GetModel() + vsModel.RSs = new(models.RealServerExpandList) + vsModel.RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) + for i, rs := range rss { + rsModel := rs.GetModel() + // rsStats := (*types.ServerStats)(rsModel.Stats) + vsModel.RSs.Items[i] = rsModel + // vsStats.Increase(rsStats) + } + shareSnapshot.ServiceUpsert(vsModel) + } + } + h.logger.Error("Virtual service version miss match.", "VipPort", params.VipPort, "correct version", version, "url query param version", params.Version) return apiVs.NewPutVsVipPortRsHealthUnexpected().WithPayload(vsModel) } + if shareSnapshot.ServiceLock(params.VipPort) { + defer shareSnapshot.ServiceUnlock(params.VipPort) + } + existOnly := true result := front.Edit(existOnly, validRSs, h.connPool, h.logger) switch result { case types.EDPVS_EXIST, types.EDPVS_OK: + // update Snapshot + if newRSs, err := front.Get(h.connPool, h.logger); err == nil { + rsModels := new(models.RealServerExpandList) + rsModels.Items = make([]*models.RealServerSpecExpand, len(newRSs)) + for i, rs := range newRSs { + rsModels.Items[i] = rs.GetModel() + } + + vsModel := shareSnapshot.ServiceGet(params.VipPort) + if vsModel == nil { + spec := types.NewVirtualServerSpec() + spec.ParseVipPortProto(params.VipPort) + + vss, err := spec.Get(h.connPool, h.logger) + if err != nil { + return apiVs.NewPutVsVipPortRsHealthInvalidBackend() + } + for _, vs := range vss { + front := types.NewRealServerFront() + front.ParseVipPortProto(vs.ID()) + + front.SetNumDests(vs.GetNumDests()) + + rss, err := front.Get(h.connPool, h.logger) + if err != nil { + h.logger.Error("Get real server list of virtual server failed.", "ID", vs.ID(), "Error", err.Error()) + return apiVs.NewPutVsVipPortRsHealthInvalidBackend() + } + vsModel = vs.GetModel() + vsModel.RSs = new(models.RealServerExpandList) + vsModel.RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) + for i, rs := range rss { + rsModel := rs.GetModel() + vsModel.RSs.Items[i] = rsModel + // rsStats := (*types.ServerStats)(rsModel.Stats) + // vsStats.Increase(rsStats) + } + } + } + vsModel.RSs = rsModels + shareSnapshot.ServiceUpsert(vsModel) + } + h.logger.Info("Set real server sets success.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) return apiVs.NewPutVsVipPortRsHealthOK() case types.EDPVS_NOTEXIST: diff --git a/tools/dpvs-agent/pkg/ipc/types/snapshot.go b/tools/dpvs-agent/pkg/ipc/types/snapshot.go index aea1fed22..9ad76787c 100644 --- a/tools/dpvs-agent/pkg/ipc/types/snapshot.go +++ b/tools/dpvs-agent/pkg/ipc/types/snapshot.go @@ -6,6 +6,7 @@ import ( "os" "strconv" "strings" + "sync" "time" "golang.org/x/sys/unix" @@ -14,64 +15,125 @@ import ( "github.com/hashicorp/go-hclog" ) +type ServiceSnapshot struct { + Service *models.VirtualServerSpecExpand + lock *sync.RWMutex +} + type NodeSnapshot struct { NodeSpec *models.DpvsNodeSpec - Services map[string]*models.VirtualServerSpecExpand + Snapshot map[string]*ServiceSnapshot +} + +func (snap *ServiceSnapshot) Lock() { + snap.lock.Lock() +} + +func (snap *ServiceSnapshot) Unlock() { + snap.lock.Unlock() +} + +func (snap *ServiceSnapshot) RLock() { + snap.lock.RLock() } -func (snapshot *NodeSnapshot) ServiceVersionUpdate(id string, logger hclog.Logger) { - services := snapshot.Services - logger.Info("Update server version begin.", "id", id, "services", services) - if _, exist := services[strings.ToLower(id)]; exist { - services[strings.ToLower(id)].Version = strconv.FormatInt(time.Now().UnixNano()/1e6, 10) +func (snap *ServiceSnapshot) RUnlock() { + snap.lock.RUnlock() +} + +func (node *NodeSnapshot) ServiceRLock(id string) bool { + snap, exist := node.Snapshot[strings.ToLower(id)] + if exist { + snap.RLock() + } + + return exist +} + +func (node *NodeSnapshot) ServiceRUnlock(id string) { + if snap, exist := node.Snapshot[strings.ToLower(id)]; exist { + snap.RUnlock() + } +} + +func (node *NodeSnapshot) ServiceLock(id string) bool { + snap, exist := node.Snapshot[strings.ToLower(id)] + if exist { + snap.Lock() + } + + return exist +} + +func (node *NodeSnapshot) ServiceUnlock(id string) { + if snap, exist := node.Snapshot[strings.ToLower(id)]; exist { + snap.Unlock() + } +} + +func (node *NodeSnapshot) ServiceVersionUpdate(id string, logger hclog.Logger) { + snapshot := node.Snapshot + logger.Info("Update server version begin.", "id", id, "services snapshot", snapshot) + if _, exist := snapshot[strings.ToLower(id)]; exist { + snapshot[strings.ToLower(id)].Service.Version = strconv.FormatInt(time.Now().UnixNano()/1e6, 10) return } - logger.Error("Update service version failed.", "id", id) + logger.Error("Update service version failed. Service not Exist.", "id", id) } -func (snapshot *NodeSnapshot) ServiceGet(id string) *models.VirtualServerSpecExpand { - if svc, exist := snapshot.Services[strings.ToLower(id)]; exist { - return svc +func (node *NodeSnapshot) SnapshotGet(id string) *ServiceSnapshot { + if snap, exist := node.Snapshot[strings.ToLower(id)]; exist { + return snap } return nil } -func (snapshot *NodeSnapshot) ServiceDel(id string) { - if _, exist := snapshot.Services[strings.ToLower(id)]; exist { - delete(snapshot.Services, strings.ToLower(id)) +func (node *NodeSnapshot) ServiceGet(id string) *models.VirtualServerSpecExpand { + if snap, exist := node.Snapshot[strings.ToLower(id)]; exist { + return snap.Service } + return nil } -func (snapshot *NodeSnapshot) ServiceVersion(id string) string { - if _, exist := snapshot.Services[strings.ToLower(id)]; exist { - return snapshot.Services[strings.ToLower(id)].Version +func (node *NodeSnapshot) ServiceDel(id string) { + if _, exist := node.Snapshot[strings.ToLower(id)]; exist { + delete(node.Snapshot, strings.ToLower(id)) + } +} + +func (node *NodeSnapshot) ServiceVersion(id string) string { + if _, exist := node.Snapshot[strings.ToLower(id)]; exist { + return node.Snapshot[strings.ToLower(id)].Service.Version } return strconv.FormatInt(time.Now().UnixNano()/1e6, 10) } -func (snapshot *NodeSnapshot) ServiceAdd(vs *VirtualServerSpec) { - version := snapshot.ServiceVersion(vs.ID()) +func (node *NodeSnapshot) ServiceAdd(vs *VirtualServerSpec) { + version := node.ServiceVersion(vs.ID()) - snapshot.Services[strings.ToLower(vs.ID())] = vs.GetModel() + svc := vs.GetModel() + svc.Version = version - snapshot.Services[strings.ToLower(vs.ID())].Version = version + node.Snapshot[strings.ToLower(vs.ID())] = &ServiceSnapshot{Service: svc, lock: new(sync.RWMutex)} } -func (snapshot *NodeSnapshot) ServiceUpsert(spec *models.VirtualServerSpecExpand) { +func (node *NodeSnapshot) ServiceUpsert(spec *models.VirtualServerSpecExpand) { svc := (*VirtualServerSpecExpandModel)(spec) - version := snapshot.ServiceVersion(svc.ID()) + version := node.ServiceVersion(svc.ID()) - snapshot.Services[strings.ToLower(svc.ID())] = spec + if _, exist := node.Snapshot[strings.ToLower(svc.ID())]; !exist { + node.Snapshot[strings.ToLower(svc.ID())] = &ServiceSnapshot{Service: spec, lock: new(sync.RWMutex)} + } - snapshot.Services[strings.ToLower(svc.ID())].Version = version + node.Snapshot[strings.ToLower(svc.ID())].Service.Version = version } -func (snapshot *NodeSnapshot) GetModels(logger hclog.Logger) *models.VirtualServerList { - services := &models.VirtualServerList{Items: make([]*models.VirtualServerSpecExpand, len(snapshot.Services))} +func (node *NodeSnapshot) GetModels(logger hclog.Logger) *models.VirtualServerList { + services := &models.VirtualServerList{Items: make([]*models.VirtualServerSpecExpand, len(node.Snapshot))} i := 0 - for _, svc := range snapshot.Services { - services.Items[i] = svc + for _, snap := range node.Snapshot { + services.Items[i] = snap.Service i++ } return services @@ -87,7 +149,7 @@ func (spec *VirtualServerSpecExpandModel) ID() string { return fmt.Sprintf("%s-%d-%s", spec.Addr, spec.Port, proto) } -func (snapshot *NodeSnapshot) LoadFrom(cacheFile string, logger hclog.Logger) error { +func (node *NodeSnapshot) LoadFrom(cacheFile string, logger hclog.Logger) error { content, err := os.ReadFile(cacheFile) if err != nil { logger.Error("Read dpvs service cache file failed.", "Error", err.Error()) @@ -99,19 +161,19 @@ func (snapshot *NodeSnapshot) LoadFrom(cacheFile string, logger hclog.Logger) er return err } - snapshot.NodeSpec = nodeSnapshot.NodeSpec + node.NodeSpec = nodeSnapshot.NodeSpec for _, svcModel := range nodeSnapshot.Services.Items { svc := (*VirtualServerSpecExpandModel)(svcModel) - snapshot.Services[strings.ToLower(svc.ID())] = svcModel + node.Snapshot[strings.ToLower(svc.ID())].Service = svcModel } return nil } -func (snapshot *NodeSnapshot) DumpTo(cacheFile string, logger hclog.Logger) error { +func (node *NodeSnapshot) DumpTo(cacheFile string, logger hclog.Logger) error { nodeSnapshot := &models.NodeServiceSnapshot{ - NodeSpec: snapshot.NodeSpec, - Services: snapshot.GetModels(logger), + NodeSpec: node.NodeSpec, + Services: node.GetModels(logger), } content, err := json.Marshal(nodeSnapshot) diff --git a/tools/dpvs-agent/pkg/settings/settings.go b/tools/dpvs-agent/pkg/settings/settings.go index 5027d4344..218e8302f 100644 --- a/tools/dpvs-agent/pkg/settings/settings.go +++ b/tools/dpvs-agent/pkg/settings/settings.go @@ -23,7 +23,7 @@ func setUp() { NodeSpec: &models.DpvsNodeSpec{ AnnouncePort: &models.VsAnnouncePort{}, }, - Services: make(map[string]*models.VirtualServerSpecExpand), + Snapshot: make(map[string]*types.ServiceSnapshot), } } From e2785980336ef8b5849974e0204379ef8ac8f953 Mon Sep 17 00:00:00 2001 From: huangyichen Date: Tue, 23 Jan 2024 09:33:09 +0800 Subject: [PATCH 05/63] format snapshot id --- tools/dpvs-agent/pkg/ipc/types/snapshot.go | 64 ++++++++++++++++++---- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/tools/dpvs-agent/pkg/ipc/types/snapshot.go b/tools/dpvs-agent/pkg/ipc/types/snapshot.go index 9ad76787c..d07d76ea1 100644 --- a/tools/dpvs-agent/pkg/ipc/types/snapshot.go +++ b/tools/dpvs-agent/pkg/ipc/types/snapshot.go @@ -3,6 +3,7 @@ package types import ( "encoding/json" "fmt" + "net" "os" "strconv" "strings" @@ -41,8 +42,39 @@ func (snap *ServiceSnapshot) RUnlock() { snap.lock.RUnlock() } +func (node *NodeSnapshot) snapshotID(id string) string { + items := strings.Split(id, "-") + if len(items) != 3 { + return "" + } + + proto := items[2] + svcProto := "tcp" + switch strings.ToLower(proto) { + case "udp", "tcp": + svcProto = strings.ToLower(proto) + default: + return "" + } + + port, err := strconv.Atoi(items[1]) + if err != nil { + return "" + } + vsPort := uint16(port) + + vip := net.ParseIP(items[0]) + if vip == nil { + return "" + } + + return fmt.Sprintf("%s-%d-%s", strings.ToLower(vip.String()), vsPort, svcProto) +} + func (node *NodeSnapshot) ServiceRLock(id string) bool { - snap, exist := node.Snapshot[strings.ToLower(id)] + snapID := node.snapshotID(id) + + snap, exist := node.Snapshot[strings.ToLower(snapID)] if exist { snap.RLock() } @@ -51,13 +83,15 @@ func (node *NodeSnapshot) ServiceRLock(id string) bool { } func (node *NodeSnapshot) ServiceRUnlock(id string) { - if snap, exist := node.Snapshot[strings.ToLower(id)]; exist { + snapID := node.snapshotID(id) + if snap, exist := node.Snapshot[strings.ToLower(snapID)]; exist { snap.RUnlock() } } func (node *NodeSnapshot) ServiceLock(id string) bool { - snap, exist := node.Snapshot[strings.ToLower(id)] + snapID := node.snapshotID(id) + snap, exist := node.Snapshot[strings.ToLower(snapID)] if exist { snap.Lock() } @@ -66,44 +100,50 @@ func (node *NodeSnapshot) ServiceLock(id string) bool { } func (node *NodeSnapshot) ServiceUnlock(id string) { - if snap, exist := node.Snapshot[strings.ToLower(id)]; exist { + snapID := node.snapshotID(id) + if snap, exist := node.Snapshot[strings.ToLower(snapID)]; exist { snap.Unlock() } } func (node *NodeSnapshot) ServiceVersionUpdate(id string, logger hclog.Logger) { + snapID := node.snapshotID(id) snapshot := node.Snapshot logger.Info("Update server version begin.", "id", id, "services snapshot", snapshot) - if _, exist := snapshot[strings.ToLower(id)]; exist { - snapshot[strings.ToLower(id)].Service.Version = strconv.FormatInt(time.Now().UnixNano()/1e6, 10) + if _, exist := snapshot[strings.ToLower(snapID)]; exist { + snapshot[strings.ToLower(snapID)].Service.Version = strconv.FormatInt(time.Now().UnixNano()/1e6, 10) return } logger.Error("Update service version failed. Service not Exist.", "id", id) } func (node *NodeSnapshot) SnapshotGet(id string) *ServiceSnapshot { - if snap, exist := node.Snapshot[strings.ToLower(id)]; exist { + snapID := node.snapshotID(id) + if snap, exist := node.Snapshot[strings.ToLower(snapID)]; exist { return snap } return nil } func (node *NodeSnapshot) ServiceGet(id string) *models.VirtualServerSpecExpand { - if snap, exist := node.Snapshot[strings.ToLower(id)]; exist { + snapID := node.snapshotID(id) + if snap, exist := node.Snapshot[strings.ToLower(snapID)]; exist { return snap.Service } return nil } func (node *NodeSnapshot) ServiceDel(id string) { - if _, exist := node.Snapshot[strings.ToLower(id)]; exist { - delete(node.Snapshot, strings.ToLower(id)) + snapID := node.snapshotID(id) + if _, exist := node.Snapshot[strings.ToLower(snapID)]; exist { + delete(node.Snapshot, strings.ToLower(snapID)) } } func (node *NodeSnapshot) ServiceVersion(id string) string { - if _, exist := node.Snapshot[strings.ToLower(id)]; exist { - return node.Snapshot[strings.ToLower(id)].Service.Version + snapID := node.snapshotID(id) + if _, exist := node.Snapshot[strings.ToLower(snapID)]; exist { + return node.Snapshot[strings.ToLower(snapID)].Service.Version } return strconv.FormatInt(time.Now().UnixNano()/1e6, 10) } From 3bf1c3f66914f66ea6fc2dba770495e700a745f8 Mon Sep 17 00:00:00 2001 From: huangyichen Date: Tue, 23 Jan 2024 18:45:43 +0800 Subject: [PATCH 06/63] BUGFIX: do not release the lock of snapshot if not the snapshot have not cached --- tools/dpvs-agent/cmd/ipvs/get_vs.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/dpvs-agent/cmd/ipvs/get_vs.go b/tools/dpvs-agent/cmd/ipvs/get_vs.go index 4a366544b..e59dba20e 100644 --- a/tools/dpvs-agent/cmd/ipvs/get_vs.go +++ b/tools/dpvs-agent/cmd/ipvs/get_vs.go @@ -102,6 +102,11 @@ func (h *getVs) Handle(params apiVs.GetVsParams) middleware.Responder { } } + if shareSnapshot.ServiceGet(vs.ID()) == nil { + shareSnapshot.ServiceUpsert(vsModel) + continue + } + shareSnapshot.ServiceLock(vs.ID()) shareSnapshot.ServiceUpsert(vsModel) shareSnapshot.ServiceUnlock(vs.ID()) From c2c4d8d47c9dfb6868fafb4b7ae8de40c5a3b2b4 Mon Sep 17 00:00:00 2001 From: huangyichen Date: Fri, 20 Oct 2023 13:53:16 +0800 Subject: [PATCH 07/63] NEW API(/v2/vs/{VSID}/rs/health) for dpvs-healthcheck module --- tools/dpvs-agent/dpvs-agent-api.yaml | 6 ++++-- tools/dpvs-agent/models/real_server_spec_tiny.go | 3 +++ tools/dpvs-agent/restapi/embedded_spec.go | 10 ++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tools/dpvs-agent/dpvs-agent-api.yaml b/tools/dpvs-agent/dpvs-agent-api.yaml index 799c956c9..a8dbf6182 100644 --- a/tools/dpvs-agent/dpvs-agent-api.yaml +++ b/tools/dpvs-agent/dpvs-agent-api.yaml @@ -296,6 +296,10 @@ definitions: type: "integer" format: "uint16" x-omitempty: false + consistentWeight: + type: "integer" + format: "uint16" + x-omitempty: false mode: type: "string" enum: @@ -613,8 +617,6 @@ definitions: LimitProportion: type: "integer" format: "uint32" - #Addr: - # type: "string" ProxyProtocol: type: "string" enum: diff --git a/tools/dpvs-agent/models/real_server_spec_tiny.go b/tools/dpvs-agent/models/real_server_spec_tiny.go index 592c97d95..29a7f849c 100644 --- a/tools/dpvs-agent/models/real_server_spec_tiny.go +++ b/tools/dpvs-agent/models/real_server_spec_tiny.go @@ -20,6 +20,9 @@ import ( // swagger:model RealServerSpecTiny type RealServerSpecTiny struct { + // consistent weight + ConsistentWeight uint16 `json:"consistentWeight"` + // inhibited Inhibited *bool `json:"inhibited,omitempty"` diff --git a/tools/dpvs-agent/restapi/embedded_spec.go b/tools/dpvs-agent/restapi/embedded_spec.go index 13baf4079..d2b479ee7 100644 --- a/tools/dpvs-agent/restapi/embedded_spec.go +++ b/tools/dpvs-agent/restapi/embedded_spec.go @@ -1741,6 +1741,11 @@ func init() { "RealServerSpecTiny": { "type": "object", "properties": { + "consistentWeight": { + "type": "integer", + "format": "uint16", + "x-omitempty": false + }, "inhibited": { "type": "boolean", "default": false @@ -4262,6 +4267,11 @@ func init() { "RealServerSpecTiny": { "type": "object", "properties": { + "consistentWeight": { + "type": "integer", + "format": "uint16", + "x-omitempty": false + }, "inhibited": { "type": "boolean", "default": false From dfa9c964501154462d0decdbf59c33cec0a6e235 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Tue, 24 Oct 2023 16:42:04 +0800 Subject: [PATCH 08/63] tools/healthcheck: fix rs weight update problem when rs's weight changed adminstratively When healthcheck program updating the rs's weight, it should consider: * rs's weight may change adminstratively * rs availabilty may change due to the server is closed or crashed Healthcheck stores the original weight when it sends down notification in order to restore the rs's weight when it gets healthy later. The problem is original weight is syncd from dpvs periodically and not always up-to-date, which may result in incorrect weight update in healthcheck up notification. Signed-off-by: ywc689 --- tools/dpvs-agent/dpvs-agent-api.yaml | 2 + tools/dpvs-agent/models/dest_check_spec.go | 8 +- tools/dpvs-agent/restapi/embedded_spec.go | 8 +- tools/healthcheck/pkg/helthcheck/checker.go | 37 +++++- tools/healthcheck/pkg/helthcheck/configs.go | 4 +- .../pkg/helthcheck/http_checker.go | 1 + tools/healthcheck/pkg/helthcheck/server.go | 53 ++++++-- tools/healthcheck/pkg/helthcheck/types.go | 11 +- tools/healthcheck/pkg/lb/dpvs_agent.go | 117 +++++++++++------- tools/healthcheck/pkg/lb/types.go | 21 +++- 10 files changed, 187 insertions(+), 75 deletions(-) diff --git a/tools/dpvs-agent/dpvs-agent-api.yaml b/tools/dpvs-agent/dpvs-agent-api.yaml index a8dbf6182..29eee592b 100644 --- a/tools/dpvs-agent/dpvs-agent-api.yaml +++ b/tools/dpvs-agent/dpvs-agent-api.yaml @@ -508,6 +508,8 @@ definitions: - tcp - udp - ping + - udpping + - http VirtualServerList: type: object properties: diff --git a/tools/dpvs-agent/models/dest_check_spec.go b/tools/dpvs-agent/models/dest_check_spec.go index 2e2e9cf9a..a2870aa5e 100644 --- a/tools/dpvs-agent/models/dest_check_spec.go +++ b/tools/dpvs-agent/models/dest_check_spec.go @@ -41,6 +41,12 @@ const ( // DestCheckSpecPing captures enum value "ping" DestCheckSpecPing DestCheckSpec = "ping" + + // DestCheckSpecUdpping captures enum value "udpping" + DestCheckSpecUdpping DestCheckSpec = "udpping" + + // DestCheckSpecHTTP captures enum value "http" + DestCheckSpecHTTP DestCheckSpec = "http" ) // for schema @@ -48,7 +54,7 @@ var destCheckSpecEnum []interface{} func init() { var res []DestCheckSpec - if err := json.Unmarshal([]byte(`["passive","tcp","udp","ping"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["passive","tcp","udp","ping","udpping","http"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/tools/dpvs-agent/restapi/embedded_spec.go b/tools/dpvs-agent/restapi/embedded_spec.go index d2b479ee7..ec04a7ea8 100644 --- a/tools/dpvs-agent/restapi/embedded_spec.go +++ b/tools/dpvs-agent/restapi/embedded_spec.go @@ -1440,7 +1440,9 @@ func init() { "passive", "tcp", "udp", - "ping" + "ping", + "udpping", + "http" ] }, "DpvsNodeSpec": { @@ -3966,7 +3968,9 @@ func init() { "passive", "tcp", "udp", - "ping" + "ping", + "udpping", + "http" ] }, "DpvsNodeSpec": { diff --git a/tools/healthcheck/pkg/helthcheck/checker.go b/tools/healthcheck/pkg/helthcheck/checker.go index 745924b7b..4c9a3239b 100644 --- a/tools/healthcheck/pkg/helthcheck/checker.go +++ b/tools/healthcheck/pkg/helthcheck/checker.go @@ -86,6 +86,8 @@ func (hc *Checker) Status() Status { } if hc.state == StateHealthy { status.Weight = hc.uweight + } else { + status.PrevWeight = hc.uweight } if hc.result != nil { status.Duration = hc.result.Duration @@ -95,15 +97,46 @@ func (hc *Checker) Status() Status { } func (hc *Checker) updateConfig(conf *CheckerConfig) { - hc.CheckerConfig = *conf + var state State + if len(hc.Id) == 0 { + hc.CheckerConfig = *conf + } else { + if conf.State != StateUnknown { + hc.State = conf.State + } + if conf.State == StateHealthy { + hc.Weight = conf.Weight + } + if conf.Interval > 0 { + hc.Interval = conf.Interval + } + if conf.Timeout > 0 { + hc.Timeout = conf.Timeout + } + if conf.Retry > 0 { + hc.Retry = conf.Retry + } + } + if conf.State != StateUnhealthy { hc.lock.Lock() weight := hc.uweight + state = hc.State hc.uweight = conf.Weight + hc.state = conf.State hc.lock.Unlock() if weight != conf.Weight { log.Infof("%v: user weight changed %d -> %d", hc.Id, weight, conf.Weight) } + } else { + hc.lock.Lock() + state = hc.State + hc.state = conf.State + hc.lock.Unlock() + } + + if state != conf.State { + log.Warningf("%v: healthcheck state changed externally %v -> %v", hc.Id, state, conf.State) } } @@ -222,7 +255,7 @@ func (hc *Checker) Run(start <-chan time.Time) { return case config := <-hc.update: - if hc.Interval != config.Interval { + if config.Interval > 0 && hc.Interval != config.Interval { ticker.Stop() if start != nil { <-start diff --git a/tools/healthcheck/pkg/helthcheck/configs.go b/tools/healthcheck/pkg/helthcheck/configs.go index 7759928b5..c7ac01024 100644 --- a/tools/healthcheck/pkg/helthcheck/configs.go +++ b/tools/healthcheck/pkg/helthcheck/configs.go @@ -89,6 +89,8 @@ func NewCheckerConfig(id *Id, checker CheckMethod, Timeout: timeout, Retry: retry, } - config.BindConfig(&config) + if config.CheckMethod != nil { + config.BindConfig(&config) + } return &config } diff --git a/tools/healthcheck/pkg/helthcheck/http_checker.go b/tools/healthcheck/pkg/helthcheck/http_checker.go index 97b7d5102..099d1ed2c 100644 --- a/tools/healthcheck/pkg/helthcheck/http_checker.go +++ b/tools/healthcheck/pkg/helthcheck/http_checker.go @@ -64,6 +64,7 @@ func NewHttpChecker(method, host, uri string, proxyProto int) *HttpChecker { } return &HttpChecker{ Method: method, + Host: host, Uri: uri, ResponseCodes: []HttpCodeRange{{200, 299}, {300, 399}, {400, 499}}, Response: "", diff --git a/tools/healthcheck/pkg/helthcheck/server.go b/tools/healthcheck/pkg/helthcheck/server.go index d7af16f08..bd7d06f04 100644 --- a/tools/healthcheck/pkg/helthcheck/server.go +++ b/tools/healthcheck/pkg/helthcheck/server.go @@ -27,6 +27,8 @@ import ( "github.com/iqiyi/dpvs/tools/healthcheck/pkg/utils" ) +const ResyncChanSize = 64 + // Server contains the data needed to run a healthcheck server. type Server struct { config *ServerConfig @@ -35,6 +37,7 @@ type Server struct { healthchecks map[Id]*Checker configs chan map[Id]*CheckerConfig notify chan *Notification + resync chan *CheckerConfig quit chan bool } @@ -63,6 +66,7 @@ func NewServer(cfg *ServerConfig) *Server { healthchecks: make(map[Id]*Checker), notify: make(chan *Notification, cfg.NotifyChannelSize), configs: make(chan map[Id]*CheckerConfig), + resync: make(chan *CheckerConfig, ResyncChanSize), quit: make(chan bool, 1), } @@ -78,8 +82,10 @@ func (s *Server) NewChecker(typ lb.Checker, proto utils.IPProto) CheckMethod { checker = NewUDPChecker("", "", 0) case lb.CheckerPING: checker = NewPingChecker() - case lb.CheckerUDPPing: + case lb.CheckerUDPPING: checker = NewUDPPingChecker("", "", 0) + case lb.CheckerHTTP: + checker = NewHttpChecker("", "", "", 0) case lb.CheckerNone: if s.config.LbAutoMethod { switch proto { @@ -111,10 +117,11 @@ func (s *Server) getHealthchecks() (*Checkers, error) { } weight := rs.Weight state := StateUnknown - if weight > 0 && rs.Inhibited == false { - state = StateHealthy - } else if weight == 0 && rs.Inhibited == true { + // Notes: rs can be down adminstratively, don't consider its weight for health state + if rs.Inhibited { state = StateUnhealthy + } else { + state = StateHealthy } // TODO: allow users to specify check interval, timeout and retry config := NewCheckerConfig(id, checker, @@ -165,16 +172,34 @@ func (s *Server) notifier() { Id: notification.Id.Vs(), Protocol: notification.Target.Proto, RSs: []lb.RealServer{{ - IP: notification.Target.IP, - Port: notification.Target.Port, - Weight: notification.Status.Weight, - Inhibited: inhibited, + IP: notification.Target.IP, + Port: notification.Target.Port, + Weight: notification.Status.Weight, + PrevWeight: notification.Status.PrevWeight, + Inhibited: inhibited, }}, } - if err := s.comm.UpdateByChecker([]lb.VirtualService{*vs}); err != nil { + if changed, err := s.comm.UpdateByChecker(*vs); err != nil { log.Warningf("Failed to Update %v healthy status to %v(weight: %d): %v", notification.Id, notification.State, notification.Status.Weight, err) + } else if len(changed) > 0 { + log.Warningf("%v:%s has changed, resync config %v ...", + notification.Id, notification.Target, changed) + // resync updated targets + for _, rs := range changed { + id := notification.Id + target := &Target{rs.IP, rs.Port, vs.Protocol} + weight := rs.Weight + state := StateUnknown + if rs.Inhibited { + state = StateUnhealthy + } else { + state = StateHealthy + } + config := NewCheckerConfig(&id, nil, target, state, weight, 0, 0, 0) + s.resync <- config + } } } } @@ -205,8 +230,8 @@ func (s *Server) manager() { hc := NewChecker(s.notify, conf.State, conf.Weight) hc.SetDryrun(s.config.DryRun) s.healthchecks[id] = hc - checkTicker := time.NewTicker(time.Duration((1 + rand.Intn( - int(DefaultCheckConfig.Interval.Milliseconds())))) * time.Millisecond) + checkTicker := time.NewTicker(time.Duration(1+rand.Intn(int( + DefaultCheckConfig.Interval.Milliseconds()))) * time.Millisecond) go hc.Run(checkTicker.C) } } @@ -215,7 +240,11 @@ func (s *Server) manager() { for id, hc := range s.healthchecks { hc.Update(configs[id]) } - + case conf := <-s.resync: + hc := s.healthchecks[conf.Id] + if hc != nil { + hc.Update(conf) + } case <-notifyTicker.C: log.Infof("Total checkers: %d", len(s.healthchecks)) // Send notifications when status changed. diff --git a/tools/healthcheck/pkg/helthcheck/types.go b/tools/healthcheck/pkg/helthcheck/types.go index 560ebc84e..a27f50685 100644 --- a/tools/healthcheck/pkg/helthcheck/types.go +++ b/tools/healthcheck/pkg/helthcheck/types.go @@ -206,8 +206,9 @@ type Status struct { Failures uint64 Successes uint64 State - Weight uint16 - Message string + Weight uint16 + PrevWeight uint16 + Message string } // Notification stores a status notification for a healthcheck. @@ -219,7 +220,7 @@ type Notification struct { // String returns the string representation for the given notification. func (n *Notification) String() string { - return fmt.Sprintf("ID %v, %v, Weight %d, Fail %v, Success %v, Last check %s in %v", n.Id, - stateNames[n.Status.State], n.Status.Weight, n.Status.Failures, n.Status.Successes, - n.Status.LastCheck.Format("2006-01-02 15:04:05.000"), n.Status.Duration) + return fmt.Sprintf("ID %v, %v, Weight %d, PrevWeight %d Fail %v, Success %v, Last check %s in %v", + n.Id, stateNames[n.Status.State], n.Status.Weight, n.Status.PrevWeight, n.Status.Failures, + n.Status.Successes, n.Status.LastCheck.Format("2006-01-02 15:04:05.000"), n.Status.Duration) } diff --git a/tools/healthcheck/pkg/lb/dpvs_agent.go b/tools/healthcheck/pkg/lb/dpvs_agent.go index 29d403430..a9e95f39d 100644 --- a/tools/healthcheck/pkg/lb/dpvs_agent.go +++ b/tools/healthcheck/pkg/lb/dpvs_agent.go @@ -32,7 +32,7 @@ var _ Comm = (*DpvsAgentComm)(nil) var ( serverDefault = "localhost:53225" listUri = LbApi{"/v2/vs", http.MethodGet} - noticeUri = LbApi{"/v2/vs/%s/rs?healthcheck=true", http.MethodPut} + noticeUri = LbApi{"/v2/vs/%s/rs/health", http.MethodPut} client *http.Client = &http.Client{Timeout: httpClientTimeout} ) @@ -50,10 +50,11 @@ type LbApi struct { } type DpvsAgentRs struct { - IP string `json:"ip"` - Port uint16 `json:"port"` - Weight uint16 `json:"weight"` - Inhibited bool `json:"inhibited,omitempty"` + IP string `json:"ip"` + Port uint16 `json:"port"` + Weight uint16 `json:"weight"` + ConsistentWeight uint16 `json:"consistentWeight,omitempty"` + Inhibited bool `json:"inhibited,omitempty"` } type DpvsAgentRsItem struct { @@ -105,6 +106,10 @@ func (avs *DpvsAgentVs) toVs() (*VirtualService, error) { checker = CheckerUDP case "ping": checker = CheckerPING + case "udpping": + checker = CheckerUDPPING + case "http": + checker = CheckerHTTP } } vs := &VirtualService{ @@ -115,19 +120,10 @@ func (avs *DpvsAgentVs) toVs() (*VirtualService, error) { RSs: make([]RealServer, len(avs.Rss.Items)), } vs.Id = avs.serviceId() - - for i, ars := range avs.Rss.Items { - rip := net.ParseIP(ars.Spec.IP) - if rip == nil { - return nil, fmt.Errorf("%s: invalid Rs IP %q", vs.Id, ars.Spec.IP) - } - rs := &RealServer{ - IP: rip, - Port: ars.Spec.Port, - Weight: ars.Spec.Weight, - Inhibited: ars.Spec.Inhibited, - } - vs.RSs[i] = *rs + if rss, err := avs.Rss.toRsList(); err != nil { + return nil, fmt.Errorf("%s: %v", vs.Id, err) + } else { + vs.RSs = rss } return vs, nil } @@ -147,6 +143,25 @@ func (avslist *DpvsAgentVsList) toVsList() ([]VirtualService, error) { return vslist, nil } +func (arsl *DpvsAgentRsList) toRsList() ([]RealServer, error) { + rss := make([]RealServer, len(arsl.Items)) + for i, ars := range arsl.Items { + rip := net.ParseIP(ars.Spec.IP) + if rip == nil { + return nil, fmt.Errorf("invalid RS IP %q", ars.Spec.IP) + } + rs := &RealServer{ + IP: rip, + Port: ars.Spec.Port, + Weight: ars.Spec.Weight, + PrevWeight: ars.Spec.ConsistentWeight, + Inhibited: ars.Spec.Inhibited, + } + rss[i] = *rs + } + return rss, nil +} + func NewDpvsAgentComm(server string) *DpvsAgentComm { if len(server) == 0 { server = serverDefault @@ -189,40 +204,50 @@ func (comm *DpvsAgentComm) ListVirtualServices() ([]VirtualService, error) { return vslist, nil } -func (comm *DpvsAgentComm) UpdateByChecker(targets []VirtualService) error { - // TODO: support batch operation - for _, vs := range targets { - for _, rs := range vs.RSs { - ars := &DpvsAgentRsListPut{ - Items: []DpvsAgentRs{ - { - IP: rs.IP.String(), - Port: rs.Port, - Weight: rs.Weight, - Inhibited: rs.Inhibited, - }, +func (comm *DpvsAgentComm) UpdateByChecker(vs VirtualService) ([]RealServer, error) { + for _, rs := range vs.RSs { + ars := &DpvsAgentRsListPut{ + Items: []DpvsAgentRs{ + { + IP: rs.IP.String(), + Port: rs.Port, + Weight: rs.Weight, + ConsistentWeight: rs.PrevWeight, + Inhibited: rs.Inhibited, }, - } - data, err := json.Marshal(ars) + }, + } + data, err := json.Marshal(ars) + if err != nil { + return nil, err + } + for _, notice := range comm.noticeApis { + url := fmt.Sprintf(notice.Url, vs.Id) + req, err := http.NewRequest(notice.HttpMethod, url, bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + resp, err := client.Do(req) if err != nil { - return err + return nil, err } - for _, notice := range comm.noticeApis { - url := fmt.Sprintf(notice.Url, vs.Id) - req, err := http.NewRequest(notice.HttpMethod, url, bytes.NewBuffer(data)) - req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - return err + //fmt.Printf("Request: %v, Code: %v\n", req, resp.Status) + if resp.StatusCode != 200 { + if data, err = io.ReadAll(resp.Body); err != nil { + return nil, fmt.Errorf("CODE: %v", resp.StatusCode) } - //fmt.Println("Code:", resp.Status) - if resp.StatusCode != 200 { - data, _ = io.ReadAll(resp.Body) - return fmt.Errorf("CODE: %v, ERROR: %s", resp.StatusCode, strings.TrimSpace(string(data))) + var rss DpvsAgentRsList + if err = json.Unmarshal(data, &rss); err != nil { + return nil, fmt.Errorf("CODE: %v, ERROR: %s", resp.StatusCode, + strings.TrimSpace(string(data))) + } + ret, err := rss.toRsList() + if err != nil { + //fmt.Println("Data:", data, "len(RSs): ", len(rss.Items), "RSs:", rss) + return nil, fmt.Errorf("CODE: %v, Error: %v", resp.StatusCode, err) } - resp.Body.Close() + return ret, nil } + resp.Body.Close() } } - return nil + return nil, nil } diff --git a/tools/healthcheck/pkg/lb/types.go b/tools/healthcheck/pkg/lb/types.go index 092f4fb22..da5ca7abf 100644 --- a/tools/healthcheck/pkg/lb/types.go +++ b/tools/healthcheck/pkg/lb/types.go @@ -27,14 +27,16 @@ const ( CheckerTCP CheckerUDP CheckerPING - CheckerUDPPing + CheckerUDPPING + CheckerHTTP ) type RealServer struct { - IP net.IP - Port uint16 - Weight uint16 - Inhibited bool + IP net.IP + Port uint16 + Weight uint16 // current weight in dpvs for ListVirtualServices, target weight for UpdateByChecker + PrevWeight uint16 // current weight in healthcheck for UpdateByChecker + Inhibited bool } type VirtualService struct { @@ -47,8 +49,11 @@ type VirtualService struct { } type Comm interface { + // Get the list of VS/RS prepared for healthcheck. ListVirtualServices() ([]VirtualService, error) - UpdateByChecker(targets []VirtualService) error + // Update RSs health state, return nil error and the lastest info of RSs whose + // weight have been changed administively on success, or error on failure. + UpdateByChecker(targets VirtualService) ([]RealServer, error) } func (checker Checker) String() string { @@ -61,6 +66,10 @@ func (checker Checker) String() string { return "checker_udp" case CheckerPING: return "checker_ping" + case CheckerUDPPING: + return "checker_udpping" + case CheckerHTTP: + return "checker_http" } return "checker_unknown" } From 1806cd0c1829c94fff3796a1d4b112201d74fb3a Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 19 Jan 2024 17:31:35 +0800 Subject: [PATCH 09/63] tools/healthcheck: further fix the problem discussed in previous commit Unfortunately, the CAS solution in the last commit is not effective for healthcheck process cannot maintain the correct previous weight value. Thus resource version is used instead, which should be passed to the lb api when the healthcheck try to update rs, and the api would reject the request if the resouce version is outdated. use vs version to resync target status when notification fails Signed-off-by: ywc689 --- tools/dpvs-agent/dpvs-agent-api.yaml | 4 -- .../models/real_server_spec_tiny.go | 3 - tools/dpvs-agent/restapi/embedded_spec.go | 10 ---- tools/healthcheck/pkg/helthcheck/checker.go | 14 +++-- tools/healthcheck/pkg/helthcheck/configs.go | 11 +++- tools/healthcheck/pkg/helthcheck/server.go | 55 ++++++++++--------- tools/healthcheck/pkg/helthcheck/types.go | 33 ++--------- tools/healthcheck/pkg/lb/dpvs_agent.go | 47 +++++++++------- tools/healthcheck/pkg/lb/dpvs_agent_test.go | 24 ++++---- tools/healthcheck/pkg/lb/types.go | 12 ++-- 10 files changed, 93 insertions(+), 120 deletions(-) diff --git a/tools/dpvs-agent/dpvs-agent-api.yaml b/tools/dpvs-agent/dpvs-agent-api.yaml index 29eee592b..f0830658f 100644 --- a/tools/dpvs-agent/dpvs-agent-api.yaml +++ b/tools/dpvs-agent/dpvs-agent-api.yaml @@ -296,10 +296,6 @@ definitions: type: "integer" format: "uint16" x-omitempty: false - consistentWeight: - type: "integer" - format: "uint16" - x-omitempty: false mode: type: "string" enum: diff --git a/tools/dpvs-agent/models/real_server_spec_tiny.go b/tools/dpvs-agent/models/real_server_spec_tiny.go index 29a7f849c..592c97d95 100644 --- a/tools/dpvs-agent/models/real_server_spec_tiny.go +++ b/tools/dpvs-agent/models/real_server_spec_tiny.go @@ -20,9 +20,6 @@ import ( // swagger:model RealServerSpecTiny type RealServerSpecTiny struct { - // consistent weight - ConsistentWeight uint16 `json:"consistentWeight"` - // inhibited Inhibited *bool `json:"inhibited,omitempty"` diff --git a/tools/dpvs-agent/restapi/embedded_spec.go b/tools/dpvs-agent/restapi/embedded_spec.go index ec04a7ea8..50bf3a4d4 100644 --- a/tools/dpvs-agent/restapi/embedded_spec.go +++ b/tools/dpvs-agent/restapi/embedded_spec.go @@ -1743,11 +1743,6 @@ func init() { "RealServerSpecTiny": { "type": "object", "properties": { - "consistentWeight": { - "type": "integer", - "format": "uint16", - "x-omitempty": false - }, "inhibited": { "type": "boolean", "default": false @@ -4271,11 +4266,6 @@ func init() { "RealServerSpecTiny": { "type": "object", "properties": { - "consistentWeight": { - "type": "integer", - "format": "uint16", - "x-omitempty": false - }, "inhibited": { "type": "boolean", "default": false diff --git a/tools/healthcheck/pkg/helthcheck/checker.go b/tools/healthcheck/pkg/helthcheck/checker.go index 4c9a3239b..19c6c286c 100644 --- a/tools/healthcheck/pkg/helthcheck/checker.go +++ b/tools/healthcheck/pkg/helthcheck/checker.go @@ -35,7 +35,7 @@ var ( ) // Checks provides a map of healthcheck configurations. -type Checkers struct { +type CheckerConfigs struct { Configs map[Id]*CheckerConfig } @@ -79,6 +79,7 @@ func (hc *Checker) Status() Status { hc.lock.RLock() defer hc.lock.RUnlock() status := Status{ + Version: hc.Version, LastCheck: hc.start, Failures: hc.failures, Successes: hc.successes, @@ -86,8 +87,6 @@ func (hc *Checker) Status() Status { } if hc.state == StateHealthy { status.Weight = hc.uweight - } else { - status.PrevWeight = hc.uweight } if hc.result != nil { status.Duration = hc.result.Duration @@ -101,6 +100,10 @@ func (hc *Checker) updateConfig(conf *CheckerConfig) { if len(hc.Id) == 0 { hc.CheckerConfig = *conf } else { + if conf.Version < hc.Version { + return + } + hc.Version = conf.Version if conf.State != StateUnknown { hc.State = conf.State } @@ -187,8 +190,10 @@ func (hc *Checker) healthcheck() { } status := "SUCCESS" + state := StateHealthy if !result.Success { status = "FAILURE" + state = StateUnhealthy } log.Infof("%v: %s: %v", hc.Id, status, result) @@ -197,15 +202,12 @@ func (hc *Checker) healthcheck() { hc.start = start hc.result = result - var state State if result.Success { - state = StateHealthy hc.failed = 0 hc.successes++ } else { hc.failed++ hc.failures++ - state = StateUnhealthy } if hc.state == StateHealthy && hc.failed > 0 && hc.failed <= uint64(hc.CheckerConfig.Retry) { diff --git a/tools/healthcheck/pkg/helthcheck/configs.go b/tools/healthcheck/pkg/helthcheck/configs.go index c7ac01024..375261cb6 100644 --- a/tools/healthcheck/pkg/helthcheck/configs.go +++ b/tools/healthcheck/pkg/helthcheck/configs.go @@ -63,6 +63,10 @@ func DefaultServerConfig() ServerConfig { type CheckerConfig struct { Id + // Version denotes the virtual service version. It used to protect the vs from + // incorrect weight updates by healthcheck when the vs's weight changed externally. + Version uint64 + Target State Weight uint16 @@ -76,11 +80,12 @@ type CheckerConfig struct { var DefaultCheckConfig CheckerConfig // NewConfig returns an initialised Config. -func NewCheckerConfig(id *Id, checker CheckMethod, - target *Target, state State, weight uint16, - interval, timeout time.Duration, retry uint) *CheckerConfig { +func NewCheckerConfig(id *Id, version uint64, checker CheckMethod, + target *Target, state State, weight uint16, interval, + timeout time.Duration, retry uint) *CheckerConfig { config := CheckerConfig{ Id: *id, + Version: version, Target: *target, State: state, Weight: weight, diff --git a/tools/healthcheck/pkg/helthcheck/server.go b/tools/healthcheck/pkg/helthcheck/server.go index bd7d06f04..3afa128eb 100644 --- a/tools/healthcheck/pkg/helthcheck/server.go +++ b/tools/healthcheck/pkg/helthcheck/server.go @@ -27,8 +27,6 @@ import ( "github.com/iqiyi/dpvs/tools/healthcheck/pkg/utils" ) -const ResyncChanSize = 64 - // Server contains the data needed to run a healthcheck server. type Server struct { config *ServerConfig @@ -66,7 +64,7 @@ func NewServer(cfg *ServerConfig) *Server { healthchecks: make(map[Id]*Checker), notify: make(chan *Notification, cfg.NotifyChannelSize), configs: make(chan map[Id]*CheckerConfig), - resync: make(chan *CheckerConfig, ResyncChanSize), + resync: make(chan *CheckerConfig, cfg.NotifyChannelSize), quit: make(chan bool, 1), } @@ -100,12 +98,12 @@ func (s *Server) NewChecker(typ lb.Checker, proto utils.IPProto) CheckMethod { } // getHealthchecks attempts to get the current healthcheck configurations from DPVS -func (s *Server) getHealthchecks() (*Checkers, error) { +func (s *Server) getHealthchecks() (*CheckerConfigs, error) { vss, err := s.comm.ListVirtualServices() if err != nil { return nil, err } - results := &Checkers{Configs: make(map[Id]*CheckerConfig)} + results := &CheckerConfigs{Configs: make(map[Id]*CheckerConfig)} for _, vs := range vss { for _, rs := range vs.RSs { target := &Target{rs.IP, rs.Port, vs.Protocol} @@ -117,14 +115,16 @@ func (s *Server) getHealthchecks() (*Checkers, error) { } weight := rs.Weight state := StateUnknown - // Notes: rs can be down adminstratively, don't consider its weight for health state + // Backend can be down adminstratively, so its weight + // should not be considered for health state. if rs.Inhibited { state = StateUnhealthy } else { state = StateHealthy } // TODO: allow users to specify check interval, timeout and retry - config := NewCheckerConfig(id, checker, + config := NewCheckerConfig(id, + vs.Version, checker, target, state, weight, DefaultCheckConfig.Interval, DefaultCheckConfig.Timeout, @@ -158,36 +158,35 @@ func (s *Server) updater() { // notifier batches healthcheck notifications and sends them to DPVS. func (s *Server) notifier() { - // TODO: support more concurrency and rate limit + // TODO: support a lot more concurrences and rate limit for { select { case notification := <-s.notify: log.Infof("Sending notification >>> %v", notification) - //fmt.Println("Sending notification >>>", notification) inhibited := false if notification.Status.State == StateUnhealthy { inhibited = true } vs := &lb.VirtualService{ + Version: notification.Status.Version, Id: notification.Id.Vs(), Protocol: notification.Target.Proto, RSs: []lb.RealServer{{ - IP: notification.Target.IP, - Port: notification.Target.Port, - Weight: notification.Status.Weight, - PrevWeight: notification.Status.PrevWeight, - Inhibited: inhibited, + IP: notification.Target.IP, + Port: notification.Target.Port, + Weight: notification.Status.Weight, + Inhibited: inhibited, }}, } - if changed, err := s.comm.UpdateByChecker(*vs); err != nil { + if changed, err := s.comm.UpdateByChecker(vs); err != nil { log.Warningf("Failed to Update %v healthy status to %v(weight: %d): %v", notification.Id, notification.State, notification.Status.Weight, err) - } else if len(changed) > 0 { + } else if changed != nil { log.Warningf("%v:%s has changed, resync config %v ...", - notification.Id, notification.Target, changed) - // resync updated targets - for _, rs := range changed { + notification.Id, notification.Target, *changed) + for _, rs := range changed.RSs { + version := changed.Version id := notification.Id target := &Target{rs.IP, rs.Port, vs.Protocol} weight := rs.Weight @@ -197,9 +196,14 @@ func (s *Server) notifier() { } else { state = StateHealthy } - config := NewCheckerConfig(&id, nil, target, state, weight, 0, 0, 0) + config := NewCheckerConfig(&id, version, nil, target, state, weight, 0, 0, 0) s.resync <- config } + } else { + // resync checker config to stop repeated notificaitons + config := NewCheckerConfig(¬ification.Id, notification.Version, nil, + ¬ification.Target, notification.State, notification.Weight, 0, 0, 0) + s.resync <- config } } } @@ -211,10 +215,9 @@ func (s *Server) notifier() { // the current configurations to each of the running healthchecks. func (s *Server) manager() { notifyTicker := time.NewTicker(s.config.NotifyInterval) - var configs map[Id]*CheckerConfig for { select { - case configs = <-s.configs: + case configs := <-s.configs: // Remove healthchecks that have been deleted. for id, hc := range s.healthchecks { @@ -247,11 +250,11 @@ func (s *Server) manager() { } case <-notifyTicker.C: log.Infof("Total checkers: %d", len(s.healthchecks)) - // Send notifications when status changed. - for id, hc := range s.healthchecks { + // Ssend notifications periodically when status in checker doesn't match config. + // It should get here only when the notification had failed. + for _, hc := range s.healthchecks { notification := hc.Notification() - if configs[id].State != notification.State { - // FIXME: Don't resend the notification after a successful one. + if hc.State != notification.State { hc.notify <- notification } } diff --git a/tools/healthcheck/pkg/helthcheck/types.go b/tools/healthcheck/pkg/helthcheck/types.go index a27f50685..ba99171a3 100644 --- a/tools/healthcheck/pkg/helthcheck/types.go +++ b/tools/healthcheck/pkg/helthcheck/types.go @@ -56,29 +56,6 @@ func (id Id) Rs() *Target { return NewTargetFromStr(strId[idx+1:]) } -// MethodType is the type of check method supported for now. -type MethodType int - -const ( - MethodTypeNone MethodType = iota - MethodTypeTCP - MethodTypeUDP - MethodTypePING -) - -// String returns the name for the given MethodType. -func (h MethodType) String() string { - switch h { - case MethodTypeTCP: - return "TCP" - case MethodTypeUDP: - return "UDP" - case MethodTypePING: - return "PING" - } - return "(unknown)" -} - // CheckMethod is the interface that must be implemented by a healthcheck. type CheckMethod interface { Check(target Target, timeout time.Duration) *Result @@ -201,14 +178,14 @@ func NewResult(start time.Time, msg string, success bool, err error) *Result { // Status represents the current status of a healthcheck instance. type Status struct { + Version uint64 // the vs version LastCheck time.Time Duration time.Duration Failures uint64 Successes uint64 State - Weight uint16 - PrevWeight uint16 - Message string + Weight uint16 + Message string } // Notification stores a status notification for a healthcheck. @@ -220,7 +197,7 @@ type Notification struct { // String returns the string representation for the given notification. func (n *Notification) String() string { - return fmt.Sprintf("ID %v, %v, Weight %d, PrevWeight %d Fail %v, Success %v, Last check %s in %v", - n.Id, stateNames[n.Status.State], n.Status.Weight, n.Status.PrevWeight, n.Status.Failures, + return fmt.Sprintf("ID %v, Version %d, %v, Weight %d, Fail %v, Success %v, Last check %s in %v", + n.Id, n.Version, stateNames[n.Status.State], n.Status.Weight, n.Status.Failures, n.Status.Successes, n.Status.LastCheck.Format("2006-01-02 15:04:05.000"), n.Status.Duration) } diff --git a/tools/healthcheck/pkg/lb/dpvs_agent.go b/tools/healthcheck/pkg/lb/dpvs_agent.go index a9e95f39d..ee7d34865 100644 --- a/tools/healthcheck/pkg/lb/dpvs_agent.go +++ b/tools/healthcheck/pkg/lb/dpvs_agent.go @@ -21,6 +21,7 @@ import ( "io" "net" "net/http" + "strconv" "strings" "time" @@ -32,7 +33,7 @@ var _ Comm = (*DpvsAgentComm)(nil) var ( serverDefault = "localhost:53225" listUri = LbApi{"/v2/vs", http.MethodGet} - noticeUri = LbApi{"/v2/vs/%s/rs/health", http.MethodPut} + noticeUri = LbApi{"/v2/vs/%s/rs/health?version=%d", http.MethodPut} client *http.Client = &http.Client{Timeout: httpClientTimeout} ) @@ -50,11 +51,10 @@ type LbApi struct { } type DpvsAgentRs struct { - IP string `json:"ip"` - Port uint16 `json:"port"` - Weight uint16 `json:"weight"` - ConsistentWeight uint16 `json:"consistentWeight,omitempty"` - Inhibited bool `json:"inhibited,omitempty"` + IP string `json:"ip"` + Port uint16 `json:"port"` + Weight uint16 `json:"weight"` + Inhibited bool `json:"inhibited,omitempty"` } type DpvsAgentRsItem struct { @@ -69,7 +69,9 @@ type DpvsAgentRsListPut struct { Items []DpvsAgentRs } +// refer to `tools/dpvs-agent/models/virtual_server_spec_expand.go: VirtualServerSpecExpand` type DpvsAgentVs struct { + Version string Addr string Port uint16 Proto uint16 @@ -87,6 +89,10 @@ func (avs *DpvsAgentVs) serviceId() string { } func (avs *DpvsAgentVs) toVs() (*VirtualService, error) { + version, err := strconv.ParseUint(avs.Version, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid Vs Version %q", avs.Version) + } vip := net.ParseIP(avs.Addr) if vip == nil { return nil, fmt.Errorf("invalid Vs Addr %q", avs.Addr) @@ -113,6 +119,7 @@ func (avs *DpvsAgentVs) toVs() (*VirtualService, error) { } } vs := &VirtualService{ + Version: version, Checker: checker, IP: vip, Port: vport, @@ -151,11 +158,10 @@ func (arsl *DpvsAgentRsList) toRsList() ([]RealServer, error) { return nil, fmt.Errorf("invalid RS IP %q", ars.Spec.IP) } rs := &RealServer{ - IP: rip, - Port: ars.Spec.Port, - Weight: ars.Spec.Weight, - PrevWeight: ars.Spec.ConsistentWeight, - Inhibited: ars.Spec.Inhibited, + IP: rip, + Port: ars.Spec.Port, + Weight: ars.Spec.Weight, + Inhibited: ars.Spec.Inhibited, } rss[i] = *rs } @@ -204,16 +210,15 @@ func (comm *DpvsAgentComm) ListVirtualServices() ([]VirtualService, error) { return vslist, nil } -func (comm *DpvsAgentComm) UpdateByChecker(vs VirtualService) ([]RealServer, error) { +func (comm *DpvsAgentComm) UpdateByChecker(vs *VirtualService) (*VirtualService, error) { for _, rs := range vs.RSs { ars := &DpvsAgentRsListPut{ Items: []DpvsAgentRs{ { - IP: rs.IP.String(), - Port: rs.Port, - Weight: rs.Weight, - ConsistentWeight: rs.PrevWeight, - Inhibited: rs.Inhibited, + IP: rs.IP.String(), + Port: rs.Port, + Weight: rs.Weight, + Inhibited: rs.Inhibited, }, }, } @@ -222,7 +227,7 @@ func (comm *DpvsAgentComm) UpdateByChecker(vs VirtualService) ([]RealServer, err return nil, err } for _, notice := range comm.noticeApis { - url := fmt.Sprintf(notice.Url, vs.Id) + url := fmt.Sprintf(notice.Url, vs.Id, vs.Version) req, err := http.NewRequest(notice.HttpMethod, url, bytes.NewBuffer(data)) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) @@ -234,12 +239,12 @@ func (comm *DpvsAgentComm) UpdateByChecker(vs VirtualService) ([]RealServer, err if data, err = io.ReadAll(resp.Body); err != nil { return nil, fmt.Errorf("CODE: %v", resp.StatusCode) } - var rss DpvsAgentRsList - if err = json.Unmarshal(data, &rss); err != nil { + var vs DpvsAgentVs + if err = json.Unmarshal(data, &vs); err != nil { return nil, fmt.Errorf("CODE: %v, ERROR: %s", resp.StatusCode, strings.TrimSpace(string(data))) } - ret, err := rss.toRsList() + ret, err := vs.toVs() if err != nil { //fmt.Println("Data:", data, "len(RSs): ", len(rss.Items), "RSs:", rss) return nil, fmt.Errorf("CODE: %v, Error: %v", resp.StatusCode, err) diff --git a/tools/healthcheck/pkg/lb/dpvs_agent_test.go b/tools/healthcheck/pkg/lb/dpvs_agent_test.go index 2d8e00122..1379b9483 100644 --- a/tools/healthcheck/pkg/lb/dpvs_agent_test.go +++ b/tools/healthcheck/pkg/lb/dpvs_agent_test.go @@ -26,24 +26,22 @@ func TestListAndUpdate(t *testing.T) { t.Errorf("list error: %v", err) } t.Logf("list Results: %v", vss) - if len(vss) < 2 { + if len(vss) < 1 { return } - t.Logf("Updating %v", vss[1]) - vss[1].RSs[0].Weight = 0 - vss[1].RSs[0].Inhibited = true - //vss[1].RSs[0].Port = 8081 - //vss[1].RSs[1].Weight = 100 - //vss[1].RSs[1].Inhibited = false - //vss[1].RSs[1].IP = net.ParseIP("1.2.3.4") - if err = comm.UpdateByChecker(vss[1:2]); err != nil { + t.Logf("Updating %v", vss[0]) + vss[0].RSs[0].Weight = 0 + vss[0].RSs[0].Inhibited = true + //vss[0].RSs[0].Port = 8081 + //vss[0].RSs[0].IP = net.ParseIP("1.2.3.4") + if _, err = comm.UpdateByChecker(&vss[0]); err != nil { t.Errorf("inhibit rs error: %v", err) } time.Sleep(3 * time.Second) - t.Logf("Restoring %v", vss[1]) - vss[1].RSs[0].Weight = 100 - vss[1].RSs[0].Inhibited = false - if err = comm.UpdateByChecker(vss[1:2]); err != nil { + t.Logf("Restoring %v", vss[0]) + vss[0].RSs[0].Weight = 100 + vss[0].RSs[0].Inhibited = false + if _, err = comm.UpdateByChecker(&vss[0]); err != nil { t.Errorf("restore rs error: %v", err) } } diff --git a/tools/healthcheck/pkg/lb/types.go b/tools/healthcheck/pkg/lb/types.go index da5ca7abf..1becf5e89 100644 --- a/tools/healthcheck/pkg/lb/types.go +++ b/tools/healthcheck/pkg/lb/types.go @@ -32,15 +32,15 @@ const ( ) type RealServer struct { - IP net.IP - Port uint16 - Weight uint16 // current weight in dpvs for ListVirtualServices, target weight for UpdateByChecker - PrevWeight uint16 // current weight in healthcheck for UpdateByChecker - Inhibited bool + IP net.IP + Port uint16 + Weight uint16 + Inhibited bool } type VirtualService struct { Id string + Version uint64 Checker Checker Protocol utils.IPProto Port uint16 @@ -53,7 +53,7 @@ type Comm interface { ListVirtualServices() ([]VirtualService, error) // Update RSs health state, return nil error and the lastest info of RSs whose // weight have been changed administively on success, or error on failure. - UpdateByChecker(targets VirtualService) ([]RealServer, error) + UpdateByChecker(targets *VirtualService) (*VirtualService, error) } func (checker Checker) String() string { From cba78e9e1fa6a7572ab048931f47ba6cd23d6117 Mon Sep 17 00:00:00 2001 From: huangyichen Date: Thu, 25 Jan 2024 11:41:00 +0800 Subject: [PATCH 10/63] update snapshot service info when snapshot exist --- tools/dpvs-agent/pkg/ipc/types/snapshot.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tools/dpvs-agent/pkg/ipc/types/snapshot.go b/tools/dpvs-agent/pkg/ipc/types/snapshot.go index d07d76ea1..2d22c1f7b 100644 --- a/tools/dpvs-agent/pkg/ipc/types/snapshot.go +++ b/tools/dpvs-agent/pkg/ipc/types/snapshot.go @@ -42,7 +42,7 @@ func (snap *ServiceSnapshot) RUnlock() { snap.lock.RUnlock() } -func (node *NodeSnapshot) snapshotID(id string) string { +func (node *NodeSnapshot) SnapshotID(id string) string { items := strings.Split(id, "-") if len(items) != 3 { return "" @@ -72,7 +72,7 @@ func (node *NodeSnapshot) snapshotID(id string) string { } func (node *NodeSnapshot) ServiceRLock(id string) bool { - snapID := node.snapshotID(id) + snapID := node.SnapshotID(id) snap, exist := node.Snapshot[strings.ToLower(snapID)] if exist { @@ -83,14 +83,14 @@ func (node *NodeSnapshot) ServiceRLock(id string) bool { } func (node *NodeSnapshot) ServiceRUnlock(id string) { - snapID := node.snapshotID(id) + snapID := node.SnapshotID(id) if snap, exist := node.Snapshot[strings.ToLower(snapID)]; exist { snap.RUnlock() } } func (node *NodeSnapshot) ServiceLock(id string) bool { - snapID := node.snapshotID(id) + snapID := node.SnapshotID(id) snap, exist := node.Snapshot[strings.ToLower(snapID)] if exist { snap.Lock() @@ -100,14 +100,14 @@ func (node *NodeSnapshot) ServiceLock(id string) bool { } func (node *NodeSnapshot) ServiceUnlock(id string) { - snapID := node.snapshotID(id) + snapID := node.SnapshotID(id) if snap, exist := node.Snapshot[strings.ToLower(snapID)]; exist { snap.Unlock() } } func (node *NodeSnapshot) ServiceVersionUpdate(id string, logger hclog.Logger) { - snapID := node.snapshotID(id) + snapID := node.SnapshotID(id) snapshot := node.Snapshot logger.Info("Update server version begin.", "id", id, "services snapshot", snapshot) if _, exist := snapshot[strings.ToLower(snapID)]; exist { @@ -118,7 +118,7 @@ func (node *NodeSnapshot) ServiceVersionUpdate(id string, logger hclog.Logger) { } func (node *NodeSnapshot) SnapshotGet(id string) *ServiceSnapshot { - snapID := node.snapshotID(id) + snapID := node.SnapshotID(id) if snap, exist := node.Snapshot[strings.ToLower(snapID)]; exist { return snap } @@ -126,7 +126,7 @@ func (node *NodeSnapshot) SnapshotGet(id string) *ServiceSnapshot { } func (node *NodeSnapshot) ServiceGet(id string) *models.VirtualServerSpecExpand { - snapID := node.snapshotID(id) + snapID := node.SnapshotID(id) if snap, exist := node.Snapshot[strings.ToLower(snapID)]; exist { return snap.Service } @@ -134,14 +134,14 @@ func (node *NodeSnapshot) ServiceGet(id string) *models.VirtualServerSpecExpand } func (node *NodeSnapshot) ServiceDel(id string) { - snapID := node.snapshotID(id) + snapID := node.SnapshotID(id) if _, exist := node.Snapshot[strings.ToLower(snapID)]; exist { delete(node.Snapshot, strings.ToLower(snapID)) } } func (node *NodeSnapshot) ServiceVersion(id string) string { - snapID := node.snapshotID(id) + snapID := node.SnapshotID(id) if _, exist := node.Snapshot[strings.ToLower(snapID)]; exist { return node.Snapshot[strings.ToLower(snapID)].Service.Version } @@ -164,6 +164,8 @@ func (node *NodeSnapshot) ServiceUpsert(spec *models.VirtualServerSpecExpand) { if _, exist := node.Snapshot[strings.ToLower(svc.ID())]; !exist { node.Snapshot[strings.ToLower(svc.ID())] = &ServiceSnapshot{Service: spec, lock: new(sync.RWMutex)} + } else { + node.Snapshot[strings.ToLower(svc.ID())].Service = spec } node.Snapshot[strings.ToLower(svc.ID())].Service.Version = version From 3bc93ddd6c5e04d75be3501ce0f2182502831ec9 Mon Sep 17 00:00:00 2001 From: huangyichen Date: Thu, 25 Jan 2024 19:48:02 +0800 Subject: [PATCH 11/63] more debug log --- tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go | 1 + tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go | 5 ++++- tools/dpvs-agent/pkg/ipc/types/snapshot.go | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go index 5f68ce323..87f1ca393 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go @@ -62,6 +62,7 @@ func (h *putVsRs) Handle(params apiVs.PutVsVipPortRsParams) middleware.Responder rss[i].SetInhibited(rs.Inhibited) rss[i].SetOverloaded(rs.Overloaded) } + h.logger.Info("Apply real server update.", "VipPort", params.VipPort, "rss", rss) } shareSnapshot := settings.ShareSnapshot() diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go index d36e66f66..b8db24b9d 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go @@ -82,12 +82,15 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa if _, existed := activeRSs[newRs.ID()]; existed { validRSs = append(validRSs, newRs) + from := activeRSs[newRs.ID()] + to := newRs + h.logger.Info("real server update.", "ID", newRs.ID(), "client Version", params.Version, "from", from, "to", to) } } } if !strings.EqualFold(params.Version, version) { - h.logger.Info("The service", "VipPort", params.VipPort, "version expired. The newest version", version) + h.logger.Info("The service", "VipPort", params.VipPort, "version expired. The latest version", version) if shareSnapshot.ServiceRLock(params.VipPort) { defer shareSnapshot.ServiceRUnlock(params.VipPort) } diff --git a/tools/dpvs-agent/pkg/ipc/types/snapshot.go b/tools/dpvs-agent/pkg/ipc/types/snapshot.go index 2d22c1f7b..b6b3bbe61 100644 --- a/tools/dpvs-agent/pkg/ipc/types/snapshot.go +++ b/tools/dpvs-agent/pkg/ipc/types/snapshot.go @@ -111,7 +111,11 @@ func (node *NodeSnapshot) ServiceVersionUpdate(id string, logger hclog.Logger) { snapshot := node.Snapshot logger.Info("Update server version begin.", "id", id, "services snapshot", snapshot) if _, exist := snapshot[strings.ToLower(snapID)]; exist { + expireVersion := snapshot[strings.ToLower(snapID)].Service.Version snapshot[strings.ToLower(snapID)].Service.Version = strconv.FormatInt(time.Now().UnixNano()/1e6, 10) + latestVersion := snapshot[strings.ToLower(snapID)].Service.Version + + logger.Info("Service version update done.", "expireVersion", expireVersion, "latest Version", latestVersion) return } logger.Error("Update service version failed. Service not Exist.", "id", id) @@ -178,6 +182,8 @@ func (node *NodeSnapshot) GetModels(logger hclog.Logger) *models.VirtualServerLi services.Items[i] = snap.Service i++ } + + logger.Info("services", services) return services } From 180e1fb52531cf847d82c9a2c3ed7e79ad0a1f57 Mon Sep 17 00:00:00 2001 From: donghaobo Date: Wed, 31 Jan 2024 16:07:40 +0800 Subject: [PATCH 12/63] synproxy random number --- src/ipvs/ip_vs_synproxy.c | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/ipvs/ip_vs_synproxy.c b/src/ipvs/ip_vs_synproxy.c index 902c4d55c..5378083b7 100644 --- a/src/ipvs/ip_vs_synproxy.c +++ b/src/ipvs/ip_vs_synproxy.c @@ -16,6 +16,9 @@ * */ #include +#include +#include +#include #include #include #include @@ -117,15 +120,35 @@ static int second_timer_expire(void *priv) } #endif +static int generate_random_key(void *key, unsigned length) +{ + int fd; + int ret; + + fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) { + return -1; + } + ret = read(fd, key, length); + close(fd); + + if (ret != (signed)length) { + return -1; + } + return 0; +} + int dp_vs_synproxy_init(void) { int i; char ack_mbufpool_name[32]; struct timeval tv; - for (i = 0; i < MD5_LBLOCK; i++) { - g_net_secret[0][i] = (uint32_t)random(); - g_net_secret[1][i] = (uint32_t)random(); + if (generate_random_key(g_net_secret, sizeof(g_net_secret))) { + for (i = 0; i < MD5_LBLOCK; i++) { + g_net_secret[0][i] = (uint32_t)random(); + g_net_secret[1][i] = (uint32_t)random(); + } } rte_atomic32_set(&g_minute_count, (uint32_t)random()); From c2d2863774dae8a99124f719e83b633c5fbba0ac Mon Sep 17 00:00:00 2001 From: huangyichen Date: Tue, 30 Jan 2024 17:13:12 +0800 Subject: [PATCH 13/63] clean healthcheck inhibited default --- src/ipvs/ip_vs_dest.c | 4 ++++ tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go | 8 +++++++- tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go | 8 +++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ipvs/ip_vs_dest.c b/src/ipvs/ip_vs_dest.c index 8564c6ebe..b5e0c8575 100644 --- a/src/ipvs/ip_vs_dest.c +++ b/src/ipvs/ip_vs_dest.c @@ -74,6 +74,10 @@ static void __dp_vs_dest_update(struct dp_vs_service *svc, int conn_flags; rte_atomic16_set(&dest->weight, udest->weight); + if (udest->flags & DPVS_DEST_F_INHIBITED) + dp_vs_dest_set_inhibited(dest); + else + dp_vs_dest_clear_inhibited(dest); conn_flags = udest->conn_flags | DPVS_CONN_F_INACTIVE; dest->fwdmode = udest->fwdmode; rte_atomic16_set(&dest->conn_flags, conn_flags); diff --git a/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go index 8665668a1..32ed0aa34 100644 --- a/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go @@ -56,9 +56,15 @@ func (h *postVsRs) Handle(params apiVs.PostVsVipPortRsParams) middleware.Respond rss[i].SetWeight(uint32(rs.Weight)) rss[i].SetProto(front.GetProto()) rss[i].SetAddr(rs.IP) - rss[i].SetInhibited(rs.Inhibited) rss[i].SetOverloaded(rs.Overloaded) rss[i].SetFwdMode(fwdmode) + // NOTE: inhibited set by healthcheck module with API /vs/${ID}/rs/health only + // we clear it default + inhibited := false + if rs.Inhibited != nil { + inhibited = *rs.Inhibited + } + rss[i].SetInhibited(&inhibited) } shareSnapshot := settings.ShareSnapshot() diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go index 87f1ca393..b0d78b288 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go @@ -59,8 +59,14 @@ func (h *putVsRs) Handle(params apiVs.PutVsVipPortRsParams) middleware.Responder rss[i].SetProto(front.GetProto()) rss[i].SetWeight(uint32(rs.Weight)) rss[i].SetFwdMode(fwdmode) - rss[i].SetInhibited(rs.Inhibited) rss[i].SetOverloaded(rs.Overloaded) + // NOTE: inhibited set by healthcheck module with API /vs/${ID}/rs/health only + // we clear it default + inhibited := false + if rs.Inhibited != nil { + inhibited = *rs.Inhibited + } + rss[i].SetInhibited(&inhibited) } h.logger.Info("Apply real server update.", "VipPort", params.VipPort, "rss", rss) } From a392793e1f7431df00661cf76303ae3192254d27 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Wed, 21 Feb 2024 15:06:00 +0800 Subject: [PATCH 14/63] tools/healthcheck: fix serveral problems - update config version finally in updateConfig and only when the backend config is in healthy state - fix notification resync fail problem caused by insufficient channel size - fix array index problem in function NewTargetFromStr Signed-off-by: ywc689 --- tools/healthcheck/pkg/helthcheck/checker.go | 66 ++++++++++++--------- tools/healthcheck/pkg/helthcheck/server.go | 8 ++- tools/healthcheck/pkg/helthcheck/types.go | 14 ++++- tools/healthcheck/pkg/lb/dpvs_agent.go | 14 +++-- 4 files changed, 64 insertions(+), 38 deletions(-) diff --git a/tools/healthcheck/pkg/helthcheck/checker.go b/tools/healthcheck/pkg/helthcheck/checker.go index 19c6c286c..66585976b 100644 --- a/tools/healthcheck/pkg/helthcheck/checker.go +++ b/tools/healthcheck/pkg/helthcheck/checker.go @@ -24,7 +24,7 @@ import ( log "github.com/golang/glog" ) -const uweightDefault uint16 = 1 +const uweightDefault uint16 = 100 var ( proxyProtoV1LocalCmd = "PROXY UNKNOWN\r\n" @@ -69,7 +69,7 @@ func NewChecker(notify chan<- *Notification, state State, weight uint16) *Checke state: state, uweight: weight, notify: notify, - update: make(chan CheckerConfig, 1), + update: make(chan CheckerConfig, 2), quit: make(chan bool, 1), } } @@ -96,20 +96,34 @@ func (hc *Checker) Status() Status { } func (hc *Checker) updateConfig(conf *CheckerConfig) { - var state State + //log.Infof("[updateConfig] id(%v) version(%d->%d) target(%v) %v->%v weight(%d->%d)\n", conf.Id, + // hc.Version, conf.Version, conf.Target, hc.State, conf.State, hc.Weight, conf.Weight) + if len(hc.Id) == 0 { hc.CheckerConfig = *conf - } else { - if conf.Version < hc.Version { - return - } - hc.Version = conf.Version - if conf.State != StateUnknown { - hc.State = conf.State - } - if conf.State == StateHealthy { - hc.Weight = conf.Weight + return + } + + if conf.Version < hc.Version { + return + } + + if conf.State == StateUnhealthy { + // Only update checker's State when the conf's state is unhealthy. + // Note that the conf's version should NOT be updated. + hc.State = conf.State + hc.lock.Lock() + state := hc.state + hc.state = conf.State + hc.lock.Unlock() + if state != conf.State { + log.Warningf("%v: healthcheck's state changed externally %v -> %v", + hc.Id, state, conf.State) } + } else { + // Update all the checker configs when conf's state is healthy. + hc.State = conf.State + hc.Weight = conf.Weight if conf.Interval > 0 { hc.Interval = conf.Interval } @@ -119,27 +133,23 @@ func (hc *Checker) updateConfig(conf *CheckerConfig) { if conf.Retry > 0 { hc.Retry = conf.Retry } - } - if conf.State != StateUnhealthy { hc.lock.Lock() + state := hc.state weight := hc.uweight - state = hc.State - hc.uweight = conf.Weight hc.state = conf.State + hc.uweight = conf.Weight hc.lock.Unlock() + + hc.Version = conf.Version if weight != conf.Weight { - log.Infof("%v: user weight changed %d -> %d", hc.Id, weight, conf.Weight) + log.Warningf("%v: healthcheck's user weight changed %d -> %d", + hc.Id, weight, conf.Weight) + } + if state != conf.State { + log.Warningf("%v: healthcheck's state changed externally %v -> %v", + hc.Id, state, conf.State) } - } else { - hc.lock.Lock() - state = hc.State - hc.state = conf.State - hc.lock.Unlock() - } - - if state != conf.State { - log.Warningf("%v: healthcheck state changed externally %v -> %v", hc.Id, state, conf.State) } } @@ -290,6 +300,6 @@ func (hc *Checker) Update(config *CheckerConfig) { select { case hc.update <- *config: default: - log.Warningf("Unable to update %v, last update still queued", hc.Id) + log.Warningf("Unable to update %v, last two update still queued", hc.Id) } } diff --git a/tools/healthcheck/pkg/helthcheck/server.go b/tools/healthcheck/pkg/helthcheck/server.go index 3afa128eb..7f5c4dc88 100644 --- a/tools/healthcheck/pkg/helthcheck/server.go +++ b/tools/healthcheck/pkg/helthcheck/server.go @@ -183,12 +183,13 @@ func (s *Server) notifier() { log.Warningf("Failed to Update %v healthy status to %v(weight: %d): %v", notification.Id, notification.State, notification.Status.Weight, err) } else if changed != nil { - log.Warningf("%v:%s has changed, resync config %v ...", - notification.Id, notification.Target, *changed) for _, rs := range changed.RSs { version := changed.Version id := notification.Id target := &Target{rs.IP, rs.Port, vs.Protocol} + if !target.Equal(id.Rs()) { + continue + } weight := rs.Weight state := StateUnknown if rs.Inhibited { @@ -196,8 +197,11 @@ func (s *Server) notifier() { } else { state = StateHealthy } + log.Warningf("%v::%s has changed, resync config %v ...", + notification.Id, notification.Target, rs) config := NewCheckerConfig(&id, version, nil, target, state, weight, 0, 0, 0) s.resync <- config + break } } else { // resync checker config to stop repeated notificaitons diff --git a/tools/healthcheck/pkg/helthcheck/types.go b/tools/healthcheck/pkg/helthcheck/types.go index ba99171a3..a45578836 100644 --- a/tools/healthcheck/pkg/helthcheck/types.go +++ b/tools/healthcheck/pkg/helthcheck/types.go @@ -100,11 +100,11 @@ func NewTargetFromStr(str string) *Target { if idx1 < 0 || idx2 < 0 || idx1 >= idx2 { return nil } - port, err := strconv.ParseUint(str[idx2:], 10, 16) + port, err := strconv.ParseUint(str[idx2+1:], 10, 16) if err != nil { return nil } - proto := utils.IPProtoFromStr(str[idx1:idx2]) + proto := utils.IPProtoFromStr(str[idx1+1 : idx2]) if proto == 0 { return nil } @@ -123,6 +123,16 @@ func (t Target) String() string { return fmt.Sprintf("[%v]:%v:%d", t.IP, t.Proto, t.Port) } +func (t *Target) Equal(t2 *Target) bool { + if t2 == nil { + return false + } + if t.Port != t2.Port || t.Proto != t2.Proto { + return false + } + return t.IP.Equal(t2.IP) +} + // Addr returns the IP:Port representation of a healthcheck target func (t Target) Addr() string { if t.IP.To4() != nil { diff --git a/tools/healthcheck/pkg/lb/dpvs_agent.go b/tools/healthcheck/pkg/lb/dpvs_agent.go index ee7d34865..6634b14be 100644 --- a/tools/healthcheck/pkg/lb/dpvs_agent.go +++ b/tools/healthcheck/pkg/lb/dpvs_agent.go @@ -54,7 +54,7 @@ type DpvsAgentRs struct { IP string `json:"ip"` Port uint16 `json:"port"` Weight uint16 `json:"weight"` - Inhibited bool `json:"inhibited,omitempty"` + Inhibited *bool `json:"inhibited,omitempty"` } type DpvsAgentRsItem struct { @@ -158,10 +158,12 @@ func (arsl *DpvsAgentRsList) toRsList() ([]RealServer, error) { return nil, fmt.Errorf("invalid RS IP %q", ars.Spec.IP) } rs := &RealServer{ - IP: rip, - Port: ars.Spec.Port, - Weight: ars.Spec.Weight, - Inhibited: ars.Spec.Inhibited, + IP: rip, + Port: ars.Spec.Port, + Weight: ars.Spec.Weight, + } + if ars.Spec.Inhibited != nil { + rs.Inhibited = *ars.Spec.Inhibited } rss[i] = *rs } @@ -218,7 +220,7 @@ func (comm *DpvsAgentComm) UpdateByChecker(vs *VirtualService) (*VirtualService, IP: rs.IP.String(), Port: rs.Port, Weight: rs.Weight, - Inhibited: rs.Inhibited, + Inhibited: &rs.Inhibited, }, }, } From e18c3331a7a469bba0da1f04337e0306d8236031 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Thu, 22 Feb 2024 09:20:55 +0800 Subject: [PATCH 15/63] tools/healthcheck: fix a deadlock problem caused by notification resync Signed-off-by: ywc689 --- tools/healthcheck/pkg/helthcheck/server.go | 20 +++++++++++------ tools/healthcheck/test/stress-test.sh | 25 +++++++++++++--------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/tools/healthcheck/pkg/helthcheck/server.go b/tools/healthcheck/pkg/helthcheck/server.go index 7f5c4dc88..edcbb6561 100644 --- a/tools/healthcheck/pkg/helthcheck/server.go +++ b/tools/healthcheck/pkg/helthcheck/server.go @@ -247,14 +247,9 @@ func (s *Server) manager() { for id, hc := range s.healthchecks { hc.Update(configs[id]) } - case conf := <-s.resync: - hc := s.healthchecks[conf.Id] - if hc != nil { - hc.Update(conf) - } case <-notifyTicker.C: log.Infof("Total checkers: %d", len(s.healthchecks)) - // Ssend notifications periodically when status in checker doesn't match config. + // Send notifications periodically when status in checker doesn't match config. // It should get here only when the notification had failed. for _, hc := range s.healthchecks { notification := hc.Notification() @@ -266,12 +261,25 @@ func (s *Server) manager() { } } +func (s *Server) resyncer() { + for { + select { + case conf := <-s.resync: + hc := s.healthchecks[conf.Id] + if hc != nil { + hc.Update(conf) + } + } + } +} + // Run runs a healthcheck server. func (s *Server) Run() { log.Infof("Starting healthcheck server (%v) ...", s.config) go s.updater() go s.notifier() go s.manager() + go s.resyncer() <-s.quit } diff --git a/tools/healthcheck/test/stress-test.sh b/tools/healthcheck/test/stress-test.sh index 6ea848f85..e72f59153 100755 --- a/tools/healthcheck/test/stress-test.sh +++ b/tools/healthcheck/test/stress-test.sh @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +#dpvs_agent_server=localhost:53225 ## Step 1. echo -e "Cleaning existing services ..." @@ -23,40 +24,44 @@ now=$(date +%F.%T) echo -e "[$now] Start" ## Step 2. -echo -e "Adding test services ..." +now=$(date +%F.%T) +echo -e "[$now] Adding test services ..." rsid=5000 for i in $(seq 0 32) do for j in $(seq 1 255) do vip="192.168.${i}.${j}" - flag="-t" - #udp=$((j%2)) - #[ "$udp" -eq 1 ] && flag="-u" - #echo $vip $flag - ipvsadm -A $flag $vip:80 + #echo $vip:80 + ipvsadm -At $vip:80 + #curl -sS -X PUT "http://${dpvs_agent_server}/v2/vs/${vip}-80-tcp" -H "Content-type:application/json" -d "{\"SchedName\":\"wrr\"}" >/dev/null + ipvsadm -Pt $vip:80 -z 192.168.88.241 -F dpdk0 >/dev/null 2>&1 + #curl -X PUT "http://${dpvs_agent_server}/v2/vs/${vip}-80-tcp/laddr" -H "Content-type:application/json" -d "{\"device\":\"dpdk0\", \"addr\":\"192.168.88.241\"}" for k in $(seq 5) do seg3=$((rsid/255)) seg4=$((rsid%255)) rsid=$((rsid+1)) rip="192.168.${seg3}.${seg4}" - #echo "-> $rip" - ipvsadm -a $flag $vip:80 -r $rip:8080 -b -w 100 + #echo "-> $rip:8080" + ipvsadm -at $vip:80 -r $rip:8080 -b -w 100 + #curl -sS -X PUT "http://${dpvs_agent_server}/v2/vs/${vip}-80-tcp/rs" -H "Content-type:application/json" -d "{\"Items\":[{\"ip\":\"${rip}\", \"port\":80, \"weight\":100}]}" > /dev/null done #dpip addr add $vip/32 dev dpdk0 done done ## Step 3. +now=$(date +%F.%T) echo "" echo "****************************************" -echo -e "Start healthcheck program on your own." +echo -e "[$now] Start healthcheck program on your own." echo "****************************************" echo "" ## Step 4. -echo -e "Do Checking ..." +now=$(date +%F.%T) +echo -e "[$now] Do Checking ..." while true do now=$(date +%F.%T) From e5d71d12f8d2aa40cca97e244e81bc5eb33288eb Mon Sep 17 00:00:00 2001 From: huangyichen Date: Tue, 27 Feb 2024 14:47:41 +0800 Subject: [PATCH 16/63] dpvs-agent local cache builded depend on the user api invoke only. (eg. POST|PUT /vs/${ID}/rs, PUT /vs/${ID}) Healthcheck api /vs/${ID}/rs/health would not update local cache. GET API /vs | /vs/${ID} response dpvs-agent local cache info default and response dpvs running service detail with query param `healthcheck=true` --- tools/dpvs-agent/cmd/ipvs/get_vs.go | 64 ++---- tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go | 57 ++++-- .../cmd/ipvs/post_vs_vip_port_rs.go | 37 +++- tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go | 38 ++-- .../dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go | 32 ++- .../cmd/ipvs/put_vs_vip_port_rs_health.go | 184 +++++------------- tools/dpvs-agent/dpvs-agent-api.yaml | 7 + tools/dpvs-agent/pkg/ipc/types/snapshot.go | 12 +- tools/dpvs-agent/restapi/embedded_spec.go | 32 +++ .../virtualserver/get_vs_parameters.go | 41 +++- .../virtualserver/get_vs_responses.go | 45 +++++ .../virtualserver/get_vs_urlbuilder.go | 13 +- .../get_vs_vip_port_parameters.go | 41 +++- .../get_vs_vip_port_urlbuilder.go | 13 +- 14 files changed, 374 insertions(+), 242 deletions(-) diff --git a/tools/dpvs-agent/cmd/ipvs/get_vs.go b/tools/dpvs-agent/cmd/ipvs/get_vs.go index e59dba20e..e3247cacd 100644 --- a/tools/dpvs-agent/cmd/ipvs/get_vs.go +++ b/tools/dpvs-agent/cmd/ipvs/get_vs.go @@ -40,18 +40,28 @@ func NewGetVs(cp *pool.ConnPool, parentLogger hclog.Logger) *getVs { } func (h *getVs) Handle(params apiVs.GetVsParams) middleware.Responder { + shareSnapshot := settings.ShareSnapshot() + if params.Healthcheck != nil && !*params.Healthcheck { + return apiVs.NewGetVsOK().WithPayload(shareSnapshot.GetModels(h.logger)) + } + + // if params.Snapshot != nil && *params.Snapshot { + // shareSnapshot.DumpTo(settings.LocalConfigFile(), h.logger) + // } + front := types.NewVirtualServerFront() vss, err := front.Get(h.connPool, h.logger) if err != nil { h.logger.Error("Get virtual server list failed.", "Error", err.Error()) - // FIXME: Invalid - return apiVs.NewGetVsOK() + return apiVs.NewGetVsNoContent() } - shareSnapshot := settings.ShareSnapshot() + vsModels := models.VirtualServerList{ + Items: make([]*models.VirtualServerSpecExpand, len(vss)), + } h.logger.Info("Get all virtual server done.", "vss", vss) - for _, vs := range vss { + for i, vs := range vss { front := types.NewRealServerFront() err := front.ParseVipPortProto(vs.ID()) @@ -69,52 +79,18 @@ func (h *getVs) Handle(params apiVs.GetVsParams) middleware.Responder { h.logger.Info("Get real server list of virtual server success.", "ID", vs.ID(), "rss", rss) - vsModel := vs.GetModel() - vsStats := (*types.ServerStats)(vsModel.Stats) - vsModel.RSs = new(models.RealServerExpandList) - vsModel.RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) + vsModels.Items[i] = vs.GetModel() + vsStats := (*types.ServerStats)(vsModels.Items[i].Stats) + vsModels.Items[i].RSs = new(models.RealServerExpandList) + vsModels.Items[i].RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) for j, rs := range rss { rsModel := rs.GetModel() rsStats := (*types.ServerStats)(rsModel.Stats) - vsModel.RSs.Items[j] = rsModel + vsModels.Items[i].RSs.Items[j] = rsModel vsStats.Increase(rsStats) } - - if shareSnapshot.NodeSpec.Laddrs == nil { - laddr := types.NewLocalAddrFront() - if err := laddr.ParseVipPortProto(vs.ID()); err != nil { - // FIXME: Invalid - return apiVs.NewGetVsOK() - } - - laddrs, err := laddr.Get(h.connPool, h.logger) - if err != nil { - // FIXME: Invalid - return apiVs.NewGetVsOK() - } - - shareSnapshot.NodeSpec.Laddrs = new(models.LocalAddressExpandList) - laddrModels := shareSnapshot.NodeSpec.Laddrs - laddrModels.Items = make([]*models.LocalAddressSpecExpand, len(laddrs)) - for k, lip := range laddrs { - laddrModels.Items[k] = lip.GetModel() - } - } - - if shareSnapshot.ServiceGet(vs.ID()) == nil { - shareSnapshot.ServiceUpsert(vsModel) - continue - } - - shareSnapshot.ServiceLock(vs.ID()) - shareSnapshot.ServiceUpsert(vsModel) - shareSnapshot.ServiceUnlock(vs.ID()) - } - - if params.Snapshot != nil && *params.Snapshot { - shareSnapshot.DumpTo(settings.LocalConfigFile(), h.logger) } - return apiVs.NewGetVsOK().WithPayload(shareSnapshot.GetModels(h.logger)) + return apiVs.NewGetVsOK().WithPayload(&vsModels) } diff --git a/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go b/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go index a05cb3f0b..da9373711 100644 --- a/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go +++ b/tools/dpvs-agent/cmd/ipvs/get_vs_vip_port.go @@ -15,6 +15,8 @@ package ipvs import ( + "strings" + "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" @@ -40,10 +42,33 @@ func NewGetVsVipPort(cp *pool.ConnPool, parentLogger hclog.Logger) *getVsVipPort } func (h *getVsVipPort) Handle(params apiVs.GetVsVipPortParams) middleware.Responder { + shareSnapshot := settings.ShareSnapshot() + if params.Healthcheck != nil && !*params.Healthcheck { + vsModel := shareSnapshot.ServiceGet(params.VipPort) + if vsModel != nil { + vsModels := new(models.VirtualServerList) + vsModels.Items = make([]*models.VirtualServerSpecExpand, 1) + vsModels.Items[0] = vsModel + return apiVs.NewGetVsVipPortOK().WithPayload(vsModels) + } + } + + vaild := true var vss []*types.VirtualServerSpec spec := types.NewVirtualServerSpec() err := spec.ParseVipPortProto(params.VipPort) if err != nil { + vaild = false + if params.Healthcheck != nil && !*params.Healthcheck { + // invalid VipPort string + // respond full cache info + vsModels := shareSnapshot.GetModels(h.logger) + if len(vsModels.Items) != 0 { + return apiVs.NewGetVsVipPortOK().WithPayload(vsModels) + } + // read from dpvs memory + } + h.logger.Warn("Convert to virtual server failed. Get All virtual server.", "VipPort", params.VipPort, "Error", err.Error()) front := types.NewVirtualServerFront() vss, err = front.Get(h.connPool, h.logger) @@ -56,10 +81,9 @@ func (h *getVsVipPort) Handle(params apiVs.GetVsVipPortParams) middleware.Respon return apiVs.NewGetVsVipPortNotFound() } - shareSnapshot := settings.ShareSnapshot() - - vsModels := new(models.VirtualServerList) - vsModels.Items = make([]*models.VirtualServerSpecExpand, len(vss)) + vsModels := &models.VirtualServerList{ + Items: make([]*models.VirtualServerSpecExpand, len(vss)), + } for i, vs := range vss { front := types.NewRealServerFront() @@ -80,22 +104,31 @@ func (h *getVsVipPort) Handle(params apiVs.GetVsVipPortParams) middleware.Respon h.logger.Info("Get real server list of virtual server success.", "ID", vs.ID(), "rss", rss) vsModel := vs.GetModel() - vsModels.Items[i] = vsModel - vsStats := (*types.ServerStats)(vsModels.Items[i].Stats) - vsModels.Items[i].RSs = new(models.RealServerExpandList) - vsModels.Items[i].RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) + vsStats := (*types.ServerStats)(vsModel.Stats) + vsModel.RSs = new(models.RealServerExpandList) + vsModel.RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) for j, rs := range rss { rsModel := rs.GetModel() rsStats := (*types.ServerStats)(rsModel.Stats) - vsModels.Items[i].RSs.Items[j] = rsModel + vsModel.RSs.Items[j] = rsModel vsStats.Increase(rsStats) } + } - shareSnapshot.ServiceLock(vs.ID()) - shareSnapshot.ServiceUpsert(vsModel) - shareSnapshot.ServiceUnlock(vs.ID()) + if vaild { + targetModels := &models.VirtualServerList{ + Items: make([]*models.VirtualServerSpecExpand, 1), + } + + for _, vsModel := range vsModels.Items { + typesVsModel := (*types.VirtualServerSpecExpandModel)(vsModel) + if strings.EqualFold(spec.ID(), typesVsModel.ID()) { + targetModels.Items[0] = vsModel + return apiVs.NewGetVsVipPortOK().WithPayload(targetModels) + } + } } return apiVs.NewGetVsVipPortOK().WithPayload(vsModels) diff --git a/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go index 32ed0aa34..08da6a94f 100644 --- a/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go @@ -15,6 +15,8 @@ package ipvs import ( + "strings" + "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" @@ -76,19 +78,36 @@ func (h *postVsRs) Handle(params apiVs.PostVsVipPortRsParams) middleware.Respond switch result { case types.EDPVS_EXIST, types.EDPVS_OK: // Update Snapshot - if newRSs, err := front.Get(h.connPool, h.logger); err == nil { - rsModels := new(models.RealServerExpandList) - rsModels.Items = make([]*models.RealServerSpecExpand, len(newRSs)) - for i, rs := range newRSs { - rsModels.Items[i] = rs.GetModel() + vsModel := shareSnapshot.ServiceGet(params.VipPort) + if vsModel == nil { + spec := types.NewVirtualServerSpec() + err := spec.ParseVipPortProto(params.VipPort) + if err != nil { + h.logger.Warn("Convert to virtual server failed.", "VipPort", params.VipPort, "Error", err.Error()) + // FIXME return + } + vss, err := spec.Get(h.connPool, h.logger) + if err != nil { + h.logger.Error("Get virtual server failed.", "svc VipPort", params.VipPort, "Error", err.Error()) + // FIXME return + } + + for _, vs := range vss { + if strings.EqualFold(vs.ID(), spec.ID()) { + shareSnapshot.ServiceAdd(vs) + break + } + } + } else { + vsModel.RSs = &models.RealServerExpandList{ + Items: make([]*models.RealServerSpecExpand, len(rss)), } - vsModel := shareSnapshot.ServiceGet(params.VipPort) - if vsModel != nil { - vsModel.RSs = rsModels - shareSnapshot.ServiceUpsert(vsModel) + for i, rs := range rss { + vsModel.RSs.Items[i] = rs.GetModel() } } + shareSnapshot.ServiceVersionUpdate(params.VipPort, h.logger) h.logger.Info("Set real server to virtual server success.", "VipPort", params.VipPort, "rss", rss, "result", result.String()) diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go index 57f175c55..6e6c50115 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go @@ -17,7 +17,6 @@ package ipvs import ( "strings" - "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" "github.com/dpvs-agent/pkg/settings" @@ -97,6 +96,7 @@ func (h *putVsItem) Handle(params apiVs.PutVsVipPortParams) middleware.Responder return apiVs.NewPutVsVipPortCreated() case types.EDPVS_EXIST: h.logger.Info("The virtual server already exist! Try to update.", "VipPort", params.VipPort) + if shareSnapshot.ServiceLock(vs.ID()) { defer shareSnapshot.ServiceUnlock(vs.ID()) } @@ -107,30 +107,22 @@ func (h *putVsItem) Handle(params apiVs.PutVsVipPortParams) middleware.Responder h.logger.Error("Update virtual server failed.", "VipPort", params.VipPort, "reason", reason.String()) return apiVs.NewPutVsVipPortInvalidBackend() } + + newVsModel := vs.GetModel() + vsModel := shareSnapshot.ServiceGet(vs.ID()) + vsModel.Bps = newVsModel.Bps + vsModel.ConnTimeout = newVsModel.ConnTimeout + vsModel.LimitProportion = newVsModel.LimitProportion + vsModel.ExpireQuiescent = newVsModel.ExpireQuiescent + vsModel.Fwmark = newVsModel.Fwmark + vsModel.SynProxy = newVsModel.SynProxy + vsModel.Match = newVsModel.Match + vsModel.SchedName = newVsModel.SchedName + vsModel.Timeout = newVsModel.Timeout + vsModel.Flags = newVsModel.Flags + h.logger.Info("Update virtual server success.", "VipPort", params.VipPort) - if vss, err := vs.Get(h.connPool, h.logger); err == nil { - for _, newVs := range vss { - front := types.NewRealServerFront() - if err := front.ParseVipPortProto(newVs.ID()); err != nil { - continue - } - - vsModel := newVs.GetModel() - front.SetNumDests(newVs.GetNumDests()) - if rss, err := front.Get(h.connPool, h.logger); err != nil { - vsModel.RSs = new(models.RealServerExpandList) - vsModel.RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) - for i, rs := range rss { - vsModel.RSs.Items[i] = rs.GetModel() - } - } - - shareSnapshot.ServiceLock(newVs.ID()) - shareSnapshot.ServiceUpsert(vsModel) - shareSnapshot.ServiceUnlock(newVs.ID()) - } - } // return 200 return apiVs.NewPutVsVipPortOK() default: diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go index b0d78b288..ac0077cc3 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go @@ -15,7 +15,9 @@ package ipvs import ( - "github.com/dpvs-agent/models" + "fmt" + "strings" + "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" "github.com/dpvs-agent/pkg/settings" @@ -84,19 +86,29 @@ func (h *putVsRs) Handle(params apiVs.PutVsVipPortRsParams) middleware.Responder case types.EDPVS_EXIST, types.EDPVS_OK: h.logger.Info("Set real server sets success.", "VipPort", params.VipPort, "rss", rss, "result", result.String()) // Update Snapshot - if newRSs, err := front.Get(h.connPool, h.logger); err == nil { - rsModels := new(models.RealServerExpandList) - rsModels.Items = make([]*models.RealServerSpecExpand, len(newRSs)) - for i, rs := range newRSs { - rsModels.Items[i] = rs.GetModel() + vsModel := shareSnapshot.ServiceGet(params.VipPort) + newRSs := make([]*types.RealServerSpec, 0) + for _, newRs := range rss { + exist := false + for _, cacheRs := range vsModel.RSs.Items { + rsID := fmt.Sprintf("%s:%d", cacheRs.Spec.IP, cacheRs.Spec.Port) + if !strings.EqualFold(newRs.ID(), rsID) { + continue + } + // update weight only + cacheRs.Spec.Weight = uint16(newRs.GetWeight()) + break } - vsModel := shareSnapshot.ServiceGet(params.VipPort) - if vsModel != nil { - vsModel.RSs = rsModels - shareSnapshot.ServiceUpsert(vsModel) + if !exist { + newRSs = append(newRSs, newRs) } } + + for _, rs := range newRSs { + vsModel.RSs.Items = append(vsModel.RSs.Items, rs.GetModel()) + } + shareSnapshot.ServiceVersionUpdate(params.VipPort, h.logger) return apiVs.NewPutVsVipPortRsOK() case types.EDPVS_NOTEXIST: diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go index b8db24b9d..bc1f93b5d 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go @@ -40,7 +40,6 @@ func NewPutVsRsHealth(cp *pool.ConnPool, parentLogger hclog.Logger) *putVsRsHeal } return &putVsRsHealth{connPool: cp, logger: logger} } - func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middleware.Responder { front := types.NewRealServerFront() if err := front.ParseVipPortProto(params.VipPort); err != nil { @@ -48,150 +47,65 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa return apiVs.NewPutVsVipPortRsHealthInvalidFrontend() } + activeRSs := make(map[string]*models.RealServerSpecExpand) + var vsModel *models.VirtualServerSpecExpand shareSnapshot := settings.ShareSnapshot() - - shareSnapshot.ServiceRLock(params.VipPort) // RLock - version := shareSnapshot.ServiceVersion(params.VipPort) - // get active backends - active, err := front.Get(h.connPool, h.logger) - if err != nil { - shareSnapshot.ServiceRUnlock(params.VipPort) // RUnlock - return apiVs.NewPutVsVipPortRsHealthInvalidBackend() - } - shareSnapshot.ServiceRUnlock(params.VipPort) // RUnlock - - activeRSs := make(map[string]*types.RealServerSpec) - for _, rs := range active { - activeRSs[rs.ID()] = rs - } - - validRSs := make([]*types.RealServerSpec, 0) - if params.Rss != nil { - for _, rs := range params.Rss.Items { - var fwdmode types.DpvsFwdMode - fwdmode.FromString(rs.Mode) - newRs := types.NewRealServerSpec() - newRs.SetAf(front.GetAf()) - newRs.SetAddr(rs.IP) - newRs.SetPort(rs.Port) - newRs.SetProto(front.GetProto()) - newRs.SetWeight(uint32(rs.Weight)) - newRs.SetFwdMode(fwdmode) - newRs.SetInhibited(rs.Inhibited) - newRs.SetOverloaded(rs.Overloaded) - - if _, existed := activeRSs[newRs.ID()]; existed { - validRSs = append(validRSs, newRs) - from := activeRSs[newRs.ID()] - to := newRs - h.logger.Info("real server update.", "ID", newRs.ID(), "client Version", params.Version, "from", from, "to", to) - } - } - } - - if !strings.EqualFold(params.Version, version) { - h.logger.Info("The service", "VipPort", params.VipPort, "version expired. The latest version", version) - if shareSnapshot.ServiceRLock(params.VipPort) { - defer shareSnapshot.ServiceRUnlock(params.VipPort) - } - vsModel := shareSnapshot.ServiceGet(params.VipPort) - if vsModel == nil { - spec := types.NewVirtualServerSpec() - spec.ParseVipPortProto(params.VipPort) - - vss, err := spec.Get(h.connPool, h.logger) - if err != nil { - return apiVs.NewPutVsVipPortRsHealthInvalidBackend() + if shareSnapshot.ServiceRLock(params.VipPort) { + vsModel = shareSnapshot.ServiceGet(params.VipPort) + if vsModel != nil { + for _, rs := range vsModel.RSs.Items { + rsModel := (*types.RealServerSpecExpandModel)(rs) + activeRSs[rsModel.ID()] = rs } - for _, vs := range vss { - front := types.NewRealServerFront() - front.ParseVipPortProto(vs.ID()) - - front.SetNumDests(vs.GetNumDests()) - - rss, err := front.Get(h.connPool, h.logger) - if err != nil { - h.logger.Error("Get real server list of virtual server failed.", "ID", vs.ID(), "Error", err.Error()) - return apiVs.NewPutVsVipPortRsHealthInvalidBackend() - } - vsModel = vs.GetModel() - vsModel.RSs = new(models.RealServerExpandList) - vsModel.RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) - for i, rs := range rss { - rsModel := rs.GetModel() - // rsStats := (*types.ServerStats)(rsModel.Stats) - vsModel.RSs.Items[i] = rsModel - // vsStats.Increase(rsStats) + h.logger.Info("service activeRSs", activeRSs) + + validRSs := make([]*types.RealServerSpec, 0) + if params.Rss != nil { + for _, rs := range params.Rss.Items { + var fwdmode types.DpvsFwdMode + fwdmode.FromString(rs.Mode) + newRs := types.NewRealServerSpec() + newRs.SetAf(front.GetAf()) + newRs.SetAddr(rs.IP) + newRs.SetPort(rs.Port) + newRs.SetProto(front.GetProto()) + newRs.SetWeight(uint32(rs.Weight)) + newRs.SetFwdMode(fwdmode) + newRs.SetInhibited(rs.Inhibited) + newRs.SetOverloaded(rs.Overloaded) + h.logger.Info("new real rs ID", newRs.ID()) + if _, existed := activeRSs[newRs.ID()]; existed { + validRSs = append(validRSs, newRs) + from := activeRSs[newRs.ID()].Spec + to := newRs + h.logger.Info("real server update.", "ID", newRs.ID(), "client Version", params.Version, "from", from, "to", to) + } } - shareSnapshot.ServiceUpsert(vsModel) } - } - h.logger.Error("Virtual service version miss match.", "VipPort", params.VipPort, "correct version", version, "url query param version", params.Version) - return apiVs.NewPutVsVipPortRsHealthUnexpected().WithPayload(vsModel) - } - if shareSnapshot.ServiceLock(params.VipPort) { - defer shareSnapshot.ServiceUnlock(params.VipPort) - } - - existOnly := true - result := front.Edit(existOnly, validRSs, h.connPool, h.logger) - switch result { - case types.EDPVS_EXIST, types.EDPVS_OK: - // update Snapshot - if newRSs, err := front.Get(h.connPool, h.logger); err == nil { - rsModels := new(models.RealServerExpandList) - rsModels.Items = make([]*models.RealServerSpecExpand, len(newRSs)) - for i, rs := range newRSs { - rsModels.Items[i] = rs.GetModel() + if !strings.EqualFold(vsModel.Version, params.Version) { + h.logger.Info("The service", "VipPort", params.VipPort, "version expired. Latest Version", vsModel.Version, "Client Version", params.Version) + shareSnapshot.ServiceRUnlock(params.VipPort) + return apiVs.NewPutVsVipPortRsHealthUnexpected().WithPayload(vsModel) } - vsModel := shareSnapshot.ServiceGet(params.VipPort) - if vsModel == nil { - spec := types.NewVirtualServerSpec() - spec.ParseVipPortProto(params.VipPort) - - vss, err := spec.Get(h.connPool, h.logger) - if err != nil { - return apiVs.NewPutVsVipPortRsHealthInvalidBackend() - } - for _, vs := range vss { - front := types.NewRealServerFront() - front.ParseVipPortProto(vs.ID()) - - front.SetNumDests(vs.GetNumDests()) - - rss, err := front.Get(h.connPool, h.logger) - if err != nil { - h.logger.Error("Get real server list of virtual server failed.", "ID", vs.ID(), "Error", err.Error()) - return apiVs.NewPutVsVipPortRsHealthInvalidBackend() - } - vsModel = vs.GetModel() - vsModel.RSs = new(models.RealServerExpandList) - vsModel.RSs.Items = make([]*models.RealServerSpecExpand, len(rss)) - for i, rs := range rss { - rsModel := rs.GetModel() - vsModel.RSs.Items[i] = rsModel - // rsStats := (*types.ServerStats)(rsModel.Stats) - // vsStats.Increase(rsStats) - } + existOnly := true + result := front.Edit(existOnly, validRSs, h.connPool, h.logger) + switch result { + case types.EDPVS_EXIST, types.EDPVS_OK: + h.logger.Info("Set real server sets success.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) + return apiVs.NewPutVsVipPortRsHealthOK() + case types.EDPVS_NOTEXIST: + if existOnly { + h.logger.Error("Edit not exist real server.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) + return apiVs.NewPutVsVipPortRsHealthInvalidFrontend() } + h.logger.Error("Unreachable branch") + default: + h.logger.Error("Set real server sets failed.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) + return apiVs.NewPutVsVipPortRsHealthInvalidBackend() } - vsModel.RSs = rsModels - shareSnapshot.ServiceUpsert(vsModel) - } - - h.logger.Info("Set real server sets success.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) - return apiVs.NewPutVsVipPortRsHealthOK() - case types.EDPVS_NOTEXIST: - if existOnly { - h.logger.Error("Edit not exist real server.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) - return apiVs.NewPutVsVipPortRsHealthInvalidFrontend() } - h.logger.Error("Unreachable branch") - default: - h.logger.Error("Set real server sets failed.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) - return apiVs.NewPutVsVipPortRsHealthInvalidBackend() } return apiVs.NewPutVsVipPortRsHealthFailure() } diff --git a/tools/dpvs-agent/dpvs-agent-api.yaml b/tools/dpvs-agent/dpvs-agent-api.yaml index f0830658f..f06d79c92 100644 --- a/tools/dpvs-agent/dpvs-agent-api.yaml +++ b/tools/dpvs-agent/dpvs-agent-api.yaml @@ -948,18 +948,25 @@ paths: parameters: - "$ref": "#/parameters/stats" - "$ref": "#/parameters/snapshot" + - "$ref": "#/parameters/healthcheck" summary: "display all vip:port:proto and rsip:port list" responses: '200': description: Success schema: "$ref": "#/definitions/VirtualServerList" + '204': + description: No Content + x-go-name: NoContent + schema: + "$ref": "#/definitions/VirtualServerList" /vs/{VipPort}: get: tags: - "virtualserver" parameters: - "$ref": "#/parameters/snapshot" + - "$ref": "#/parameters/healthcheck" - "$ref": "#/parameters/service-id" - "$ref": "#/parameters/stats" summary: "get a specific virtual server" diff --git a/tools/dpvs-agent/pkg/ipc/types/snapshot.go b/tools/dpvs-agent/pkg/ipc/types/snapshot.go index b6b3bbe61..28b713b08 100644 --- a/tools/dpvs-agent/pkg/ipc/types/snapshot.go +++ b/tools/dpvs-agent/pkg/ipc/types/snapshot.go @@ -157,6 +157,9 @@ func (node *NodeSnapshot) ServiceAdd(vs *VirtualServerSpec) { svc := vs.GetModel() svc.Version = version + if svc.RSs == nil { + svc.RSs = &models.RealServerExpandList{Items: make([]*models.RealServerSpecExpand, 0)} + } node.Snapshot[strings.ToLower(vs.ID())] = &ServiceSnapshot{Service: svc, lock: new(sync.RWMutex)} } @@ -187,6 +190,12 @@ func (node *NodeSnapshot) GetModels(logger hclog.Logger) *models.VirtualServerLi return services } +type RealServerSpecExpandModel models.RealServerSpecExpand + +func (rs *RealServerSpecExpandModel) ID() string { + return fmt.Sprintf("%s:%d", net.ParseIP(rs.Spec.IP), rs.Spec.Port) +} + type VirtualServerSpecExpandModel models.VirtualServerSpecExpand func (spec *VirtualServerSpecExpandModel) ID() string { @@ -194,7 +203,8 @@ func (spec *VirtualServerSpecExpandModel) ID() string { if spec.Proto == unix.IPPROTO_UDP { proto = "udp" } - return fmt.Sprintf("%s-%d-%s", spec.Addr, spec.Port, proto) + + return fmt.Sprintf("%s-%d-%s", net.ParseIP(spec.Addr).String(), spec.Port, proto) } func (node *NodeSnapshot) LoadFrom(cacheFile string, logger hclog.Logger) error { diff --git a/tools/dpvs-agent/restapi/embedded_spec.go b/tools/dpvs-agent/restapi/embedded_spec.go index 50bf3a4d4..aef67a7fe 100644 --- a/tools/dpvs-agent/restapi/embedded_spec.go +++ b/tools/dpvs-agent/restapi/embedded_spec.go @@ -578,6 +578,9 @@ func init() { }, { "$ref": "#/parameters/snapshot" + }, + { + "$ref": "#/parameters/healthcheck" } ], "responses": { @@ -586,6 +589,13 @@ func init() { "schema": { "$ref": "#/definitions/VirtualServerList" } + }, + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/VirtualServerList" + }, + "x-go-name": "NoContent" } } } @@ -600,6 +610,9 @@ func init() { { "$ref": "#/parameters/snapshot" }, + { + "$ref": "#/parameters/healthcheck" + }, { "$ref": "#/parameters/service-id" }, @@ -2962,6 +2975,12 @@ func init() { "default": true, "name": "snapshot", "in": "query" + }, + { + "type": "boolean", + "default": false, + "name": "healthcheck", + "in": "query" } ], "responses": { @@ -2970,6 +2989,13 @@ func init() { "schema": { "$ref": "#/definitions/VirtualServerList" } + }, + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/VirtualServerList" + }, + "x-go-name": "NoContent" } } } @@ -2987,6 +3013,12 @@ func init() { "name": "snapshot", "in": "query" }, + { + "type": "boolean", + "default": false, + "name": "healthcheck", + "in": "query" + }, { "type": "string", "name": "VipPort", diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_parameters.go index 4c527457b..baac157f1 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_parameters.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_parameters.go @@ -22,11 +22,14 @@ func NewGetVsParams() GetVsParams { var ( // initialize parameters with default values - snapshotDefault = bool(true) - statsDefault = bool(false) + healthcheckDefault = bool(false) + snapshotDefault = bool(true) + statsDefault = bool(false) ) return GetVsParams{ + Healthcheck: &healthcheckDefault, + Snapshot: &snapshotDefault, Stats: &statsDefault, @@ -42,6 +45,11 @@ type GetVsParams struct { // HTTP Request Object HTTPRequest *http.Request `json:"-"` + /* + In: query + Default: false + */ + Healthcheck *bool /* In: query Default: true @@ -65,6 +73,11 @@ func (o *GetVsParams) BindRequest(r *http.Request, route *middleware.MatchedRout qs := runtime.Values(r.URL.Query()) + qHealthcheck, qhkHealthcheck, _ := qs.GetOK("healthcheck") + if err := o.bindHealthcheck(qHealthcheck, qhkHealthcheck, route.Formats); err != nil { + res = append(res, err) + } + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { res = append(res, err) @@ -80,6 +93,30 @@ func (o *GetVsParams) BindRequest(r *http.Request, route *middleware.MatchedRout return nil } +// bindHealthcheck binds and validates parameter Healthcheck from query. +func (o *GetVsParams) bindHealthcheck(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetVsParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("healthcheck", "query", "bool", raw) + } + o.Healthcheck = &value + + return nil +} + // bindSnapshot binds and validates parameter Snapshot from query. func (o *GetVsParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_responses.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_responses.go index c67eec7a7..701d342e1 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_responses.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_responses.go @@ -57,3 +57,48 @@ func (o *GetVsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Produce } } } + +// GetVsNoContentCode is the HTTP code returned for type GetVsNoContent +const GetVsNoContentCode int = 204 + +/* +GetVsNoContent No Content + +swagger:response getVsNoContent +*/ +type GetVsNoContent struct { + + /* + In: Body + */ + Payload *models.VirtualServerList `json:"body,omitempty"` +} + +// NewGetVsNoContent creates GetVsNoContent with default headers values +func NewGetVsNoContent() *GetVsNoContent { + + return &GetVsNoContent{} +} + +// WithPayload adds the payload to the get vs no content response +func (o *GetVsNoContent) WithPayload(payload *models.VirtualServerList) *GetVsNoContent { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get vs no content response +func (o *GetVsNoContent) SetPayload(payload *models.VirtualServerList) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetVsNoContent) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(204) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_urlbuilder.go index 35fc9b8f6..3c2fbac4d 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_urlbuilder.go @@ -15,8 +15,9 @@ import ( // GetVsURL generates an URL for the get vs operation type GetVsURL struct { - Snapshot *bool - Stats *bool + Healthcheck *bool + Snapshot *bool + Stats *bool _basePath string // avoid unkeyed usage @@ -52,6 +53,14 @@ func (o *GetVsURL) Build() (*url.URL, error) { qs := make(url.Values) + var healthcheckQ string + if o.Healthcheck != nil { + healthcheckQ = swag.FormatBool(*o.Healthcheck) + } + if healthcheckQ != "" { + qs.Set("healthcheck", healthcheckQ) + } + var snapshotQ string if o.Snapshot != nil { snapshotQ = swag.FormatBool(*o.Snapshot) diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_parameters.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_parameters.go index 77ef69f53..e5e06dc91 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_parameters.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_parameters.go @@ -22,11 +22,14 @@ func NewGetVsVipPortParams() GetVsVipPortParams { var ( // initialize parameters with default values - snapshotDefault = bool(true) - statsDefault = bool(false) + healthcheckDefault = bool(false) + snapshotDefault = bool(true) + statsDefault = bool(false) ) return GetVsVipPortParams{ + Healthcheck: &healthcheckDefault, + Snapshot: &snapshotDefault, Stats: &statsDefault, @@ -47,6 +50,11 @@ type GetVsVipPortParams struct { In: path */ VipPort string + /* + In: query + Default: false + */ + Healthcheck *bool /* In: query Default: true @@ -75,6 +83,11 @@ func (o *GetVsVipPortParams) BindRequest(r *http.Request, route *middleware.Matc res = append(res, err) } + qHealthcheck, qhkHealthcheck, _ := qs.GetOK("healthcheck") + if err := o.bindHealthcheck(qHealthcheck, qhkHealthcheck, route.Formats); err != nil { + res = append(res, err) + } + qSnapshot, qhkSnapshot, _ := qs.GetOK("snapshot") if err := o.bindSnapshot(qSnapshot, qhkSnapshot, route.Formats); err != nil { res = append(res, err) @@ -104,6 +117,30 @@ func (o *GetVsVipPortParams) bindVipPort(rawData []string, hasKey bool, formats return nil } +// bindHealthcheck binds and validates parameter Healthcheck from query. +func (o *GetVsVipPortParams) bindHealthcheck(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetVsVipPortParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("healthcheck", "query", "bool", raw) + } + o.Healthcheck = &value + + return nil +} + // bindSnapshot binds and validates parameter Snapshot from query. func (o *GetVsVipPortParams) bindSnapshot(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_urlbuilder.go b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_urlbuilder.go index 0344bdc38..e09106c05 100644 --- a/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_urlbuilder.go +++ b/tools/dpvs-agent/restapi/operations/virtualserver/get_vs_vip_port_urlbuilder.go @@ -18,8 +18,9 @@ import ( type GetVsVipPortURL struct { VipPort string - Snapshot *bool - Stats *bool + Healthcheck *bool + Snapshot *bool + Stats *bool _basePath string // avoid unkeyed usage @@ -62,6 +63,14 @@ func (o *GetVsVipPortURL) Build() (*url.URL, error) { qs := make(url.Values) + var healthcheckQ string + if o.Healthcheck != nil { + healthcheckQ = swag.FormatBool(*o.Healthcheck) + } + if healthcheckQ != "" { + qs.Set("healthcheck", healthcheckQ) + } + var snapshotQ string if o.Snapshot != nil { snapshotQ = swag.FormatBool(*o.Snapshot) From 134a0989851759c652625b22b599ad9e1c133f7f Mon Sep 17 00:00:00 2001 From: huangyichen Date: Tue, 27 Feb 2024 16:02:41 +0800 Subject: [PATCH 17/63] remove debug log --- tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go index bc1f93b5d..8d7d9cee3 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go @@ -57,7 +57,6 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa rsModel := (*types.RealServerSpecExpandModel)(rs) activeRSs[rsModel.ID()] = rs } - h.logger.Info("service activeRSs", activeRSs) validRSs := make([]*types.RealServerSpec, 0) if params.Rss != nil { @@ -73,7 +72,6 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa newRs.SetFwdMode(fwdmode) newRs.SetInhibited(rs.Inhibited) newRs.SetOverloaded(rs.Overloaded) - h.logger.Info("new real rs ID", newRs.ID()) if _, existed := activeRSs[newRs.ID()]; existed { validRSs = append(validRSs, newRs) from := activeRSs[newRs.ID()].Spec From 3528ef24640b0a09149bee901639e1864c6c4d46 Mon Sep 17 00:00:00 2001 From: huangyichen Date: Tue, 27 Feb 2024 20:18:15 +0800 Subject: [PATCH 18/63] release service lock --- tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go index 8d7d9cee3..503304b6a 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go @@ -51,6 +51,8 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa var vsModel *models.VirtualServerSpecExpand shareSnapshot := settings.ShareSnapshot() if shareSnapshot.ServiceRLock(params.VipPort) { + defer shareSnapshot.ServiceRUnlock(params.VipPort) + vsModel = shareSnapshot.ServiceGet(params.VipPort) if vsModel != nil { for _, rs := range vsModel.RSs.Items { @@ -83,7 +85,6 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa if !strings.EqualFold(vsModel.Version, params.Version) { h.logger.Info("The service", "VipPort", params.VipPort, "version expired. Latest Version", vsModel.Version, "Client Version", params.Version) - shareSnapshot.ServiceRUnlock(params.VipPort) return apiVs.NewPutVsVipPortRsHealthUnexpected().WithPayload(vsModel) } From 6b153c094528ca05e23721173e6fee4128b360c5 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Mon, 4 Mar 2024 20:32:07 +0800 Subject: [PATCH 19/63] update local cache real server inhibited --- tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go | 11 ++++++----- .../dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go | 11 +++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go index ac0077cc3..fb5c05884 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go @@ -92,12 +92,13 @@ func (h *putVsRs) Handle(params apiVs.PutVsVipPortRsParams) middleware.Responder exist := false for _, cacheRs := range vsModel.RSs.Items { rsID := fmt.Sprintf("%s:%d", cacheRs.Spec.IP, cacheRs.Spec.Port) - if !strings.EqualFold(newRs.ID(), rsID) { - continue + if strings.EqualFold(newRs.ID(), rsID) { + // update weight only + inhibited := newRs.GetInhibited() + cacheRs.Spec.Weight = uint16(newRs.GetWeight()) + cacheRs.Spec.Inhibited = &inhibited + break } - // update weight only - cacheRs.Spec.Weight = uint16(newRs.GetWeight()) - break } if !exist { diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go index 503304b6a..a72263a8e 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go @@ -15,6 +15,7 @@ package ipvs import ( + "fmt" "strings" "github.com/dpvs-agent/models" @@ -92,6 +93,16 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa result := front.Edit(existOnly, validRSs, h.connPool, h.logger) switch result { case types.EDPVS_EXIST, types.EDPVS_OK: + for _, newRs := range validRSs { + for _, rs := range vsModel.RSs.Items { + rsID := fmt.Sprintf("%s:%d", rs.Spec.IP, rs.Spec.Port) + if strings.EqualFold(newRs.ID(), rsID) { + inhibited := newRs.GetInhibited() + rs.Spec.Inhibited = &inhibited + break + } + } + } h.logger.Info("Set real server sets success.", "VipPort", params.VipPort, "validRSs", validRSs, "result", result.String()) return apiVs.NewPutVsVipPortRsHealthOK() case types.EDPVS_NOTEXIST: From 30fb60591fda94dff9f2b946008ee33244b50e14 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Tue, 5 Mar 2024 18:48:05 +0800 Subject: [PATCH 20/63] tools/healthcheck: fix problems in config update Signed-off-by: ywc689 --- tools/healthcheck/pkg/helthcheck/checker.go | 36 +++++++++------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/tools/healthcheck/pkg/helthcheck/checker.go b/tools/healthcheck/pkg/helthcheck/checker.go index 66585976b..e756fd7c8 100644 --- a/tools/healthcheck/pkg/helthcheck/checker.go +++ b/tools/healthcheck/pkg/helthcheck/checker.go @@ -96,6 +96,11 @@ func (hc *Checker) Status() Status { } func (hc *Checker) updateConfig(conf *CheckerConfig) { + // Note: + // The conf::Weight must be the original weight not modified by healthcheck program, + // while conf::State reflects the health state derived from the inhibited flag set by + // the healthcheck program. + //log.Infof("[updateConfig] id(%v) version(%d->%d) target(%v) %v->%v weight(%d->%d)\n", conf.Id, // hc.Version, conf.Version, conf.Target, hc.State, conf.State, hc.Weight, conf.Weight) @@ -108,21 +113,18 @@ func (hc *Checker) updateConfig(conf *CheckerConfig) { return } - if conf.State == StateUnhealthy { - // Only update checker's State when the conf's state is unhealthy. - // Note that the conf's version should NOT be updated. - hc.State = conf.State - hc.lock.Lock() - state := hc.state - hc.state = conf.State - hc.lock.Unlock() - if state != conf.State { - log.Warningf("%v: healthcheck's state changed externally %v -> %v", - hc.Id, state, conf.State) - } - } else { + hc.State = conf.State + hc.lock.Lock() + state := hc.state + hc.state = conf.State + hc.lock.Unlock() + if state != conf.State { + log.Warningf("%v: healthcheck's state changed externally %v -> %v", + hc.Id, state, conf.State) + } + + if conf.State != StateUnhealthy || conf.State == StateUnhealthy && conf.Weight > 0 { // Update all the checker configs when conf's state is healthy. - hc.State = conf.State hc.Weight = conf.Weight if conf.Interval > 0 { hc.Interval = conf.Interval @@ -135,9 +137,7 @@ func (hc *Checker) updateConfig(conf *CheckerConfig) { } hc.lock.Lock() - state := hc.state weight := hc.uweight - hc.state = conf.State hc.uweight = conf.Weight hc.lock.Unlock() @@ -146,10 +146,6 @@ func (hc *Checker) updateConfig(conf *CheckerConfig) { log.Warningf("%v: healthcheck's user weight changed %d -> %d", hc.Id, weight, conf.Weight) } - if state != conf.State { - log.Warningf("%v: healthcheck's state changed externally %v -> %v", - hc.Id, state, conf.State) - } } } From add443b8ac571d058ccf9b376f3c0b4c39454242 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Tue, 5 Mar 2024 20:43:40 +0800 Subject: [PATCH 21/63] tools/dpvs-agent: fix crash problem in reconfiguring existing virtual services --- tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go | 4 ++++ tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go | 16 ++++++++++++++++ tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go | 3 ++- .../cmd/ipvs/put_vs_vip_port_rs_health.go | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go index 08da6a94f..80bd06a39 100644 --- a/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/post_vs_vip_port_rs.go @@ -48,6 +48,10 @@ func (h *postVsRs) Handle(params apiVs.PostVsVipPortRsParams) middleware.Respond return apiVs.NewPostVsVipPortRsInvalidFrontend() } + if params.Rss == nil || params.Rss.Items == nil { + return apiVs.NewPostVsVipPortRsInvalidFrontend() + } + rss := make([]*types.RealServerSpec, len(params.Rss.Items)) for i, rs := range params.Rss.Items { var fwdmode types.DpvsFwdMode diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go index 6e6c50115..b040e2074 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go @@ -17,6 +17,7 @@ package ipvs import ( "strings" + "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" "github.com/dpvs-agent/pkg/settings" @@ -110,6 +111,14 @@ func (h *putVsItem) Handle(params apiVs.PutVsVipPortParams) middleware.Responder newVsModel := vs.GetModel() vsModel := shareSnapshot.ServiceGet(vs.ID()) + if vsModel == nil { + newVsModel.RSs = &models.RealServerExpandList{ + Items: make([]*models.RealServerSpecExpand, 0), + } + shareSnapshot.ServiceUpsert(newVsModel) + return apiVs.NewPutVsVipPortOK() + } + vsModel.Bps = newVsModel.Bps vsModel.ConnTimeout = newVsModel.ConnTimeout vsModel.LimitProportion = newVsModel.LimitProportion @@ -120,6 +129,13 @@ func (h *putVsItem) Handle(params apiVs.PutVsVipPortParams) middleware.Responder vsModel.SchedName = newVsModel.SchedName vsModel.Timeout = newVsModel.Timeout vsModel.Flags = newVsModel.Flags + if vsModel.RSs == nil { + vsModel.RSs = &models.RealServerExpandList{} + } + + if vsModel.RSs.Items == nil { + vsModel.RSs.Items = make([]*models.RealServerSpecExpand, 0) + } h.logger.Info("Update virtual server success.", "VipPort", params.VipPort) diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go index fb5c05884..0699daa5b 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs.go @@ -49,7 +49,7 @@ func (h *putVsRs) Handle(params apiVs.PutVsVipPortRsParams) middleware.Responder } var rss []*types.RealServerSpec - if params.Rss != nil { + if params.Rss != nil && params.Rss.Items != nil { rss = make([]*types.RealServerSpec, len(params.Rss.Items)) for i, rs := range params.Rss.Items { var fwdmode types.DpvsFwdMode @@ -97,6 +97,7 @@ func (h *putVsRs) Handle(params apiVs.PutVsVipPortRsParams) middleware.Responder inhibited := newRs.GetInhibited() cacheRs.Spec.Weight = uint16(newRs.GetWeight()) cacheRs.Spec.Inhibited = &inhibited + exist = true break } } diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go index a72263a8e..e86b55930 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_rs_health.go @@ -62,7 +62,7 @@ func (h *putVsRsHealth) Handle(params apiVs.PutVsVipPortRsHealthParams) middlewa } validRSs := make([]*types.RealServerSpec, 0) - if params.Rss != nil { + if params.Rss != nil && params.Rss.Items != nil { for _, rs := range params.Rss.Items { var fwdmode types.DpvsFwdMode fwdmode.FromString(rs.Mode) From 05eb6fa0930f72e51c790b3ade316dccf495d16f Mon Sep 17 00:00:00 2001 From: ywc689 Date: Mon, 11 Mar 2024 18:38:32 +0800 Subject: [PATCH 22/63] tools/healthcheck: fix bad icmp checksum problem for udp and udpping checkers Signed-off-by: ywc689 --- .../pkg/helthcheck/http_checker_test.go | 8 ++++---- .../pkg/helthcheck/ping_checker.go | 20 +++++++++++-------- .../pkg/helthcheck/ping_checker_test.go | 2 +- .../pkg/helthcheck/udp_ping_checker_test.go | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/tools/healthcheck/pkg/helthcheck/http_checker_test.go b/tools/healthcheck/pkg/helthcheck/http_checker_test.go index 0fbbff3b4..5d3b3d491 100644 --- a/tools/healthcheck/pkg/helthcheck/http_checker_test.go +++ b/tools/healthcheck/pkg/helthcheck/http_checker_test.go @@ -58,7 +58,7 @@ func TestHttpChecker(t *testing.T) { } */ id := Id(target.String()) - config := NewCheckerConfig(&id, checker, &target, StateUnknown, + config := NewCheckerConfig(&id, 0, checker, &target, StateUnknown, 0, 3*time.Second, 2*time.Second, 3) result := checker.Check(target, config.Timeout) fmt.Printf("[ HTTP ] %s ==> %v\n", target, result) @@ -68,14 +68,14 @@ func TestHttpChecker(t *testing.T) { checker := NewHttpChecker("", "", "", 1) checker.Host = target.Addr() id := Id(target.String()) - config := NewCheckerConfig(&id, checker, &target, StateUnknown, + config := NewCheckerConfig(&id, 0, checker, &target, StateUnknown, 0, 3*time.Second, 2*time.Second, 3) result := checker.Check(target, config.Timeout) fmt.Printf("[ HTTP(PPv1) ] %s ==> %v\n", target, result) checker2 := NewHttpChecker("", "", "", 2) checker2.Host = target.Addr() id2 := Id(target.String()) - config2 := NewCheckerConfig(&id2, checker2, &target, StateUnknown, + config2 := NewCheckerConfig(&id2, 0, checker2, &target, StateUnknown, 0, 3*time.Second, 2*time.Second, 3) result2 := checker2.Check(target, config2.Timeout) fmt.Printf("[ HTTP(PPv2) ] %s ==> %v\n", target, result2) @@ -90,7 +90,7 @@ func TestHttpChecker(t *testing.T) { checker.Secure = true } id := Id(host) - config := NewCheckerConfig(&id, checker, &Target{}, StateUnknown, + config := NewCheckerConfig(&id, 0, checker, &Target{}, StateUnknown, 0, 3*time.Second, 2*time.Second, 3) result := checker.Check(Target{}, config.Timeout) if result.Success == false { diff --git a/tools/healthcheck/pkg/helthcheck/ping_checker.go b/tools/healthcheck/pkg/helthcheck/ping_checker.go index dae8eda92..a1401f66d 100644 --- a/tools/healthcheck/pkg/helthcheck/ping_checker.go +++ b/tools/healthcheck/pkg/helthcheck/ping_checker.go @@ -21,6 +21,7 @@ package hc import ( "bytes" + "encoding/binary" "fmt" "math/rand" "net" @@ -113,8 +114,8 @@ func newICMPv4EchoRequest(id, seqnum, msglen uint16, filler []byte) icmpMsg { cs := icmpChecksum(msg) // place checksum back in header; using ^= avoids the assumption that the // checksum bytes are zero - msg[2] ^= uint8(cs & 0xff) - msg[3] ^= uint8(cs >> 8) + cs ^= binary.BigEndian.Uint16(msg[2:4]) + binary.BigEndian.PutUint16(msg[2:4], cs) return msg } @@ -122,13 +123,13 @@ func icmpChecksum(msg icmpMsg) uint16 { cklen := len(msg) s := uint32(0) for i := 0; i < cklen-1; i += 2 { - s += uint32(msg[i+1])<<8 | uint32(msg[i]) + s += uint32(binary.BigEndian.Uint16(msg[i : i+2])) } if cklen&1 == 1 { - s += uint32(msg[cklen-1]) + s += uint32(msg[cklen-1]) << 8 } s = (s >> 16) + (s & 0xffff) - s = s + (s >> 16) + s += (s >> 16) return uint16(^s) } @@ -175,10 +176,13 @@ func exchangeICMPEcho(network string, ip net.IP, timeout time.Duration, echo icm c.SetDeadline(time.Now().Add(timeout)) reply := make([]byte, 256) for { - _, addr, err := c.ReadFrom(reply) + n, addr, err := c.ReadFrom(reply) if err != nil { return err } + if n < 0 || n > len(reply) { + return fmt.Errorf("Unexpect ICMP reply len %d", n) + } if !ip.Equal(net.ParseIP(addr.String())) { continue } @@ -191,9 +195,9 @@ func exchangeICMPEcho(network string, ip net.IP, timeout time.Duration, echo icm continue } if reply[0] == ICMP4_ECHO_REPLY { - cs := icmpChecksum(reply) + cs := icmpChecksum(reply[:n]) if cs != 0 { - return fmt.Errorf("Bad ICMP checksum: %x", rchksum) + return fmt.Errorf("Bad ICMP checksum: %x, len: %d, data: %v", rchksum, n, reply[:n]) } } // TODO(angusc): Validate checksum for IPv6 diff --git a/tools/healthcheck/pkg/helthcheck/ping_checker_test.go b/tools/healthcheck/pkg/helthcheck/ping_checker_test.go index 7b86e665a..b462c1dd7 100644 --- a/tools/healthcheck/pkg/helthcheck/ping_checker_test.go +++ b/tools/healthcheck/pkg/helthcheck/ping_checker_test.go @@ -37,7 +37,7 @@ func TestPingChecker(t *testing.T) { for _, target := range ping_targets { checker := NewPingChecker() id := Id(target.IP.String()) - config := NewCheckerConfig(&id, checker, + config := NewCheckerConfig(&id, 0, checker, &target, StateUnknown, 0, 3*time.Second, 1*time.Second, 3) result := checker.Check(target, config.Timeout) diff --git a/tools/healthcheck/pkg/helthcheck/udp_ping_checker_test.go b/tools/healthcheck/pkg/helthcheck/udp_ping_checker_test.go index 321035336..a93bb356f 100644 --- a/tools/healthcheck/pkg/helthcheck/udp_ping_checker_test.go +++ b/tools/healthcheck/pkg/helthcheck/udp_ping_checker_test.go @@ -39,7 +39,7 @@ func TestUDPPingChecker(t *testing.T) { for _, target := range udpping_targets { checker := NewUDPPingChecker("", "", 0) id := Id(target.String()) - config := NewCheckerConfig(&id, checker, + config := NewCheckerConfig(&id, 0, checker, &target, StateUnknown, 0, 3*time.Second, 2*time.Second, 3) result := checker.Check(target, config.Timeout) From fad525d6ef49c8f76e2da66398781af05409a30d Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 15 Mar 2024 17:36:50 +0800 Subject: [PATCH 23/63] release v1.9.7 Signed-off-by: ywc689 --- src/VERSION | 40 +++++----------------------------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/src/VERSION b/src/VERSION index 7a8b5d13b..be46f5131 100755 --- a/src/VERSION +++ b/src/VERSION @@ -1,44 +1,14 @@ #!/bin/sh # program: dpvs -# Dec 19, 2023 # +# Mar 12, 2024 # ## -# Features -# - New tool: **dpvs-agent**, a management daemon tool for dpvs based on OpenAPI. -# - New tool: **healthcheck**, a service health check daemon tool cooperating with dpvs-agent. -# - Dpvs: Develop **passive health check** methods for tcp and bidirectional udp backends. -# - Dpvs: Add supports for **Proxy Protocol** with both v1 and v2 versions. -# - Dpvs: Add supports for extended statistics of ethernet devices. -# - Dpvs: Add configuration file and dpip supports for allmulticast setting switch. -# - Build: Transfer all build configurations to a top-level file `config.mk`. -# - Containerization: Draft a Dockerfile and a tutorial document to build and run dpvs in container. -# # Bugfixes -# - Dpvs: Protect toa from source address spoofing attack and increase success ratio for source address delievery via toa. -# - Dpvs: Adjust tcp window scale in outbound direction for synproxy to improve throughput in bulk upload cases. -# - Dpvs: Fix timer inaccuracy problem when timing over 524s. -# - Dpvs: Fix the crash problem caused by ether address list buffer overflow. -# - Dpvs: Fix the crash problem caused by dividing by zero when bonding slaves attempt to send packets out. -# - Dpvs: Fix the crash problem caused by inconsistent data structures of `dp_vs_dest_compat` between dpvs and keepalived. -# - Dpvs: Correct ipo option length for judgement of branching to standalone uoa. -# - Dpvs: Inhibit setting multicast ether address from slave lcores. -# - Dpvs: Fix service flag conflicts of synproxy and expire-quiescent. -# - Dpvs: Fix the chaos use of flag, flags and fwdmode in dest and service structures. -# - Dpvs: Fix service flush function not usable problem. -# - Dpvs: Fix invalid port problem when getting verbose information of netif devices. -# - Dpvs: Use atomic operation to generate packet id for ipv4 header. -# - Dpvs: Remove fragile implementations of strategy routing for snat. -# - Dpvs: Remove the stale config item "ipc_msg/unix_domain". -# - Keepalived: Do not delete and re-add vs/rs to eliminate service disturbances at reload. -# - Keepalived: Fix a carsh problem caused by missing definition of allowlist/denylist config items. -# - Ipvsadm: Add `conn-timeout` configuration option for service. -# - Ipvsadm: Fix the ambiguous use of '-Y' configuration option. -# - Ipvsadm: Fix icmpv6 configuration option `-1` lost problem.. -# - Ipvsadm: Update help text, including supported schedulers, laddr and allow/deny ip list. -# - Dpip: Fix line break problem in help message. -# - Uoa: Enable ipv6 with a macro for uoa example server. +# - tools: Fix concurrency problem between dpvs-agent and healthcheck in editing realserver . +# - tools/dpvs-agent: Add the snapshot cache. +# - tools/healthchech: Fix occasionally arising bad icmp checksum problem for udp and udpping checkers. # export VERSION=1.9 -export RELEASE=6 +export RELEASE=7 echo $VERSION-$RELEASE From aa9f6fef715a61462e3e92309266dd9e7fdfbd0c Mon Sep 17 00:00:00 2001 From: lixiaoxiao Date: Mon, 25 Mar 2024 20:17:12 +0800 Subject: [PATCH 24/63] bugfix: keepalived new_vs quorum_state_up always be true and quorum_up script will not be excuted when all old rs(healthcheck is alive) are removed and new rs(healthcheck is alive) add in same reload. --- tools/keepalived/keepalived/check/ipwrapper.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/keepalived/keepalived/check/ipwrapper.c b/tools/keepalived/keepalived/check/ipwrapper.c index d2658c067..80980b0ba 100755 --- a/tools/keepalived/keepalived/check/ipwrapper.c +++ b/tools/keepalived/keepalived/check/ipwrapper.c @@ -1024,6 +1024,12 @@ clear_diff_rs(virtual_server_t *old_vs, virtual_server_t *new_vs, list old_check } } clear_service_rs(old_vs, rs_to_remove, false); + + //keep new_vs quorum_state_up same with old_vs + if (old_vs->quorum_state_up != new_vs->quorum_state_up) { + new_vs->quorum_state_up = old_vs->quorum_state_up; + } + free_list(&rs_to_remove); } From ee8508c985306e1a7f874d5db0faf75461d88eb7 Mon Sep 17 00:00:00 2001 From: Vladimir Kuramshin Date: Thu, 28 Mar 2024 18:59:57 +0300 Subject: [PATCH 25/63] ipvs: sctp implementation --- include/conf/inet.h | 1 + include/conf/match.h | 2 + include/conf/service.h | 5 +- include/ipvs/proto_sctp.h | 66 ++ include/ipvs/stats.h | 1 + include/sctp/sctp.h | 664 ++++++++++++++++++ src/iftraf.c | 6 +- src/ipvs/ip_vs_conn.c | 38 +- src/ipvs/ip_vs_dest.c | 4 +- src/ipvs/ip_vs_laddr.c | 3 +- src/ipvs/ip_vs_nat64.c | 1 + src/ipvs/ip_vs_proto.c | 11 + src/ipvs/ip_vs_proto_sctp.c | 558 +++++++++++++++ src/ipvs/ip_vs_service.c | 3 +- src/tc/cls_match.c | 13 + tools/dpip/cls.c | 2 +- tools/ipvsadm/ipvsadm.c | 41 +- .../keepalived/keepalived/check/check_data.c | 3 + 18 files changed, 1409 insertions(+), 13 deletions(-) create mode 100644 include/ipvs/proto_sctp.h create mode 100755 include/sctp/sctp.h create mode 100644 src/ipvs/ip_vs_proto_sctp.c diff --git a/include/conf/inet.h b/include/conf/inet.h index 1c3449781..6ae6c7e9c 100644 --- a/include/conf/inet.h +++ b/include/conf/inet.h @@ -89,6 +89,7 @@ static inline const char *inet_proto_name(uint8_t proto) const static char *proto_names[256] = { [IPPROTO_TCP] = "TCP", [IPPROTO_UDP] = "UDP", + [IPPROTO_SCTP] = "SCTP", [IPPROTO_ICMP] = "ICMP", [IPPROTO_ICMPV6] = "ICMPV6", }; diff --git a/include/conf/match.h b/include/conf/match.h index 30a9d1e17..e0eac905a 100644 --- a/include/conf/match.h +++ b/include/conf/match.h @@ -93,6 +93,8 @@ static inline int parse_match(const char *pattern, uint8_t *proto, *proto = IPPROTO_TCP; } else if (strcmp(tok, "udp") == 0) { *proto = IPPROTO_UDP; + } else if (strcmp(tok, "sctp") == 0) { + *proto = IPPROTO_SCTP; } else if (strcmp(tok, "icmp") == 0) { *proto = IPPROTO_ICMP; } else if (strcmp(tok, "icmp6") == 0) { diff --git a/include/conf/service.h b/include/conf/service.h index d16164f3c..3a3279cb4 100644 --- a/include/conf/service.h +++ b/include/conf/service.h @@ -52,8 +52,9 @@ #define DEST_HC_PASSIVE 0x01 #define DEST_HC_TCP 0x02 #define DEST_HC_UDP 0x04 -#define DEST_HC_PING 0x08 -#define DEST_HC_MASK_EXTERNAL 0x0e +#define DEST_HC_SCTP 0x08 +#define DEST_HC_PING 0x10 +#define DEST_HC_MASK_EXTERNAL 0x1e /* defaults for dest passive health check */ #define DEST_DOWN_NOTICE_DEFAULT 1 diff --git a/include/ipvs/proto_sctp.h b/include/ipvs/proto_sctp.h new file mode 100644 index 000000000..232d9594d --- /dev/null +++ b/include/ipvs/proto_sctp.h @@ -0,0 +1,66 @@ +/* + * DPVS is a software load balancer (Virtual Server) based on DPDK. + * + * Copyright (C) 2021 iQIYI (www.iqiyi.com). + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +#ifndef __DP_VS_PROTO_SCTP_H__ +#define __DP_VS_PROTO_SCTP_H__ + +#include +#include "sctp/sctp.h" + +enum dpvs_sctp_event_t { + DPVS_SCTP_DATA = 0, /* DATA, SACK, HEARTBEATs */ + DPVS_SCTP_INIT, + DPVS_SCTP_INIT_ACK, + DPVS_SCTP_COOKIE_ECHO, + DPVS_SCTP_COOKIE_ACK, + DPVS_SCTP_SHUTDOWN, + DPVS_SCTP_SHUTDOWN_ACK, + DPVS_SCTP_SHUTDOWN_COMPLETE, + DPVS_SCTP_ERROR, + DPVS_SCTP_ABORT, + DPVS_SCTP_EVENT_LAST +}; + +/* ip_vs_conn handling functions + * (from ip_vs_conn.c) + */ +enum { DPVS_DIR_INPUT = 0, + DPVS_DIR_OUTPUT, + DPVS_DIR_INPUT_ONLY, + DPVS_DIR_LAST, +}; + +/* SCTP State Values */ +enum dpvs_sctp_states { + DPVS_SCTP_S_NONE, + DPVS_SCTP_S_INIT1, + DPVS_SCTP_S_INIT, + DPVS_SCTP_S_COOKIE_SENT, + DPVS_SCTP_S_COOKIE_REPLIED, + DPVS_SCTP_S_COOKIE_WAIT, + DPVS_SCTP_S_COOKIE, + DPVS_SCTP_S_COOKIE_ECHOED, + DPVS_SCTP_S_ESTABLISHED, + DPVS_SCTP_S_SHUTDOWN_SENT, + DPVS_SCTP_S_SHUTDOWN_RECEIVED, + DPVS_SCTP_S_SHUTDOWN_ACK_SENT, + DPVS_SCTP_S_REJECTED, + DPVS_SCTP_S_CLOSED, + DPVS_SCTP_S_LAST +}; + +#endif diff --git a/include/ipvs/stats.h b/include/ipvs/stats.h index 3e4f8db11..0332fd2d9 100644 --- a/include/ipvs/stats.h +++ b/include/ipvs/stats.h @@ -53,6 +53,7 @@ enum dp_vs_estats_type { SYNPROXY_CONN_REUSED_CLOSEWAIT, SYNPROXY_CONN_REUSED_LASTACK, DEFENCE_IP_FRAG_DROP, + DEFENCE_SCTP_DROP, DEFENCE_TCP_DROP, DEFENCE_UDP_DROP, FAST_XMIT_REJECT, diff --git a/include/sctp/sctp.h b/include/sctp/sctp.h new file mode 100755 index 000000000..2d2d76100 --- /dev/null +++ b/include/sctp/sctp.h @@ -0,0 +1,664 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2001-2008, by Cisco Systems, Inc. All rights reserved. + * Copyright (c) 2008-2012, by Randall Stewart. All rights reserved. + * Copyright (c) 2008-2012, by Michael Tuexen. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * a) Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * b) Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * + * c) Neither the name of Cisco Systems, Inc. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if defined(__FreeBSD__) && !defined(__Userspace__) +#include +__FBSDID("$FreeBSD$"); +#endif + +#ifndef _NETINET_SCTP_H_ +#define _NETINET_SCTP_H_ + +#if defined(__APPLE__) || defined(__linux__) +#include +#endif +#include + +#if !defined(_WIN32) +#define SCTP_PACKED __attribute__((packed)) +#else +#pragma pack (push, 1) +#define SCTP_PACKED +#endif + +/* + * SCTP protocol - RFC4960. + */ +struct sctphdr { + uint16_t src_port; /* source port */ + uint16_t dest_port; /* destination port */ + uint32_t v_tag; /* verification tag of packet */ + uint32_t checksum; /* CRC32C checksum */ + /* chunks follow... */ +} SCTP_PACKED; + +/* + * SCTP Chunks + */ +struct sctp_chunkhdr { + uint8_t chunk_type; /* chunk type */ + uint8_t chunk_flags; /* chunk flags */ + uint16_t chunk_length; /* chunk length */ + /* optional params follow */ +} SCTP_PACKED; + +/* + * SCTP chunk parameters + */ +struct sctp_paramhdr { + uint16_t param_type; /* parameter type */ + uint16_t param_length; /* parameter length */ +} SCTP_PACKED; + +/* + * user socket options: socket API defined + */ +/* + * read-write options + */ +#define SCTP_RTOINFO 0x00000001 +#define SCTP_ASSOCINFO 0x00000002 +#define SCTP_INITMSG 0x00000003 +#define SCTP_NODELAY 0x00000004 +#define SCTP_AUTOCLOSE 0x00000005 +#define SCTP_SET_PEER_PRIMARY_ADDR 0x00000006 +#define SCTP_PRIMARY_ADDR 0x00000007 +#define SCTP_ADAPTATION_LAYER 0x00000008 +/* same as above */ +#define SCTP_ADAPTION_LAYER 0x00000008 +#define SCTP_DISABLE_FRAGMENTS 0x00000009 +#define SCTP_PEER_ADDR_PARAMS 0x0000000a +#define SCTP_DEFAULT_SEND_PARAM 0x0000000b +/* ancillary data/notification interest options */ +#define SCTP_EVENTS 0x0000000c /* deprecated */ +/* Without this applied we will give V4 and V6 addresses on a V6 socket */ +#define SCTP_I_WANT_MAPPED_V4_ADDR 0x0000000d +#define SCTP_MAXSEG 0x0000000e +#define SCTP_DELAYED_SACK 0x0000000f +#define SCTP_FRAGMENT_INTERLEAVE 0x00000010 +#define SCTP_PARTIAL_DELIVERY_POINT 0x00000011 +/* authentication support */ +#define SCTP_AUTH_CHUNK 0x00000012 +#define SCTP_AUTH_KEY 0x00000013 +#define SCTP_HMAC_IDENT 0x00000014 +#define SCTP_AUTH_ACTIVE_KEY 0x00000015 +#define SCTP_AUTH_DELETE_KEY 0x00000016 +#define SCTP_USE_EXT_RCVINFO 0x00000017 +#define SCTP_AUTO_ASCONF 0x00000018 /* rw */ +#define SCTP_MAXBURST 0x00000019 /* rw */ +#define SCTP_MAX_BURST 0x00000019 /* rw */ +/* assoc level context */ +#define SCTP_CONTEXT 0x0000001a /* rw */ +/* explicit EOR signalling */ +#define SCTP_EXPLICIT_EOR 0x0000001b +#define SCTP_REUSE_PORT 0x0000001c /* rw */ +#define SCTP_AUTH_DEACTIVATE_KEY 0x0000001d +#define SCTP_EVENT 0x0000001e +#define SCTP_RECVRCVINFO 0x0000001f +#define SCTP_RECVNXTINFO 0x00000020 +#define SCTP_DEFAULT_SNDINFO 0x00000021 +#define SCTP_DEFAULT_PRINFO 0x00000022 +#define SCTP_PEER_ADDR_THLDS 0x00000023 +#define SCTP_REMOTE_UDP_ENCAPS_PORT 0x00000024 +#define SCTP_ECN_SUPPORTED 0x00000025 +#define SCTP_PR_SUPPORTED 0x00000026 +#define SCTP_AUTH_SUPPORTED 0x00000027 +#define SCTP_ASCONF_SUPPORTED 0x00000028 +#define SCTP_RECONFIG_SUPPORTED 0x00000029 +#define SCTP_NRSACK_SUPPORTED 0x00000030 +#define SCTP_PKTDROP_SUPPORTED 0x00000031 +#define SCTP_MAX_CWND 0x00000032 + +/* + * read-only options + */ +#define SCTP_STATUS 0x00000100 +#define SCTP_GET_PEER_ADDR_INFO 0x00000101 +/* authentication support */ +#define SCTP_PEER_AUTH_CHUNKS 0x00000102 +#define SCTP_LOCAL_AUTH_CHUNKS 0x00000103 +#define SCTP_GET_ASSOC_NUMBER 0x00000104 /* ro */ +#define SCTP_GET_ASSOC_ID_LIST 0x00000105 /* ro */ +#define SCTP_TIMEOUTS 0x00000106 +#define SCTP_PR_STREAM_STATUS 0x00000107 +#define SCTP_PR_ASSOC_STATUS 0x00000108 + +/* + * user socket options: BSD implementation specific + */ +/* + * Blocking I/O is enabled on any TCP type socket by default. For the UDP + * model if this is turned on then the socket buffer is shared for send + * resources amongst all associations. The default for the UDP model is that + * is SS_NBIO is set. Which means all associations have a separate send + * limit BUT they will NOT ever BLOCK instead you will get an error back + * EAGAIN if you try to send too much. If you want the blocking semantics you + * set this option at the cost of sharing one socket send buffer size amongst + * all associations. Peeled off sockets turn this option off and block. But + * since both TCP and peeled off sockets have only one assoc per socket this + * is fine. It probably does NOT make sense to set this on SS_NBIO on a TCP + * model OR peeled off UDP model, but we do allow you to do so. You just use + * the normal syscall to toggle SS_NBIO the way you want. + * + * Blocking I/O is controlled by the SS_NBIO flag on the socket state so_state + * field. + */ + +#define SCTP_ENABLE_STREAM_RESET 0x00000900 /* struct sctp_assoc_value */ +#define SCTP_RESET_STREAMS 0x00000901 /* struct sctp_reset_streams */ +#define SCTP_RESET_ASSOC 0x00000902 /* sctp_assoc_t */ +#define SCTP_ADD_STREAMS 0x00000903 /* struct sctp_add_streams */ + +/* For enable stream reset */ +#define SCTP_ENABLE_RESET_STREAM_REQ 0x00000001 +#define SCTP_ENABLE_RESET_ASSOC_REQ 0x00000002 +#define SCTP_ENABLE_CHANGE_ASSOC_REQ 0x00000004 +#define SCTP_ENABLE_VALUE_MASK 0x00000007 +/* For reset streams */ +#define SCTP_STREAM_RESET_INCOMING 0x00000001 +#define SCTP_STREAM_RESET_OUTGOING 0x00000002 + +/* here on down are more implementation specific */ +#define SCTP_SET_DEBUG_LEVEL 0x00001005 +#define SCTP_CLR_STAT_LOG 0x00001007 +/* CMT ON/OFF socket option */ +#define SCTP_CMT_ON_OFF 0x00001200 +#define SCTP_CMT_USE_DAC 0x00001201 +/* JRS - Pluggable Congestion Control Socket option */ +#define SCTP_PLUGGABLE_CC 0x00001202 +/* RS - Pluggable Stream Scheduling Socket option */ +#define SCTP_PLUGGABLE_SS 0x00001203 +#define SCTP_SS_VALUE 0x00001204 +#define SCTP_CC_OPTION 0x00001205 /* Options for CC modules */ +/* For I-DATA */ +#define SCTP_INTERLEAVING_SUPPORTED 0x00001206 + +/* read only */ +#define SCTP_GET_SNDBUF_USE 0x00001101 +#define SCTP_GET_STAT_LOG 0x00001103 +#define SCTP_PCB_STATUS 0x00001104 +#define SCTP_GET_NONCE_VALUES 0x00001105 + +/* Special hook for dynamically setting primary for all assoc's, + * this is a write only option that requires root privilege. + */ +#define SCTP_SET_DYNAMIC_PRIMARY 0x00002001 + +/* VRF (virtual router feature) and multi-VRF support + * options. VRF's provide splits within a router + * that give the views of multiple routers. A + * standard host, without VRF support, is just + * a single VRF. If VRF's are supported then + * the transport must be VRF aware. This means + * that every socket call coming in must be directed + * within the endpoint to one of the VRF's it belongs + * to. The endpoint, before binding, may select + * the "default" VRF it is in by using a set socket + * option with SCTP_VRF_ID. This will also + * get propagated to the default VRF. Once the + * endpoint binds an address then it CANNOT add + * additional VRF's to become a Multi-VRF endpoint. + * + * Before BINDING additional VRF's can be added with + * the SCTP_ADD_VRF_ID call or deleted with + * SCTP_DEL_VRF_ID. + * + * Associations are ALWAYS contained inside a single + * VRF. They cannot reside in two (or more) VRF's. Incoming + * packets, assuming the router is VRF aware, can always + * tell us what VRF they arrived on. A host not supporting + * any VRF's will find that the packets always arrived on the + * single VRF that the host has. + * + */ + +#define SCTP_VRF_ID 0x00003001 +#define SCTP_ADD_VRF_ID 0x00003002 +#define SCTP_GET_VRF_IDS 0x00003003 +#define SCTP_GET_ASOC_VRF 0x00003004 +#define SCTP_DEL_VRF_ID 0x00003005 + +/* + * If you enable packet logging you can get + * a poor mans ethereal output in binary + * form. Note this is a compile option to + * the kernel, SCTP_PACKET_LOGGING, and + * without it in your kernel you + * will get a EOPNOTSUPP + */ +#define SCTP_GET_PACKET_LOG 0x00004001 + +/* + * hidden implementation specific options these are NOT user visible (should + * move out of sctp.h) + */ +/* sctp_bindx() flags as hidden socket options */ +#define SCTP_BINDX_ADD_ADDR 0x00008001 +#define SCTP_BINDX_REM_ADDR 0x00008002 +/* Hidden socket option that gets the addresses */ +#define SCTP_GET_PEER_ADDRESSES 0x00008003 +#define SCTP_GET_LOCAL_ADDRESSES 0x00008004 +/* return the total count in bytes needed to hold all local addresses bound */ +#define SCTP_GET_LOCAL_ADDR_SIZE 0x00008005 +/* Return the total count in bytes needed to hold the remote address */ +#define SCTP_GET_REMOTE_ADDR_SIZE 0x00008006 +/* hidden option for connectx */ +#define SCTP_CONNECT_X 0x00008007 +/* hidden option for connectx_delayed, part of sendx */ +#define SCTP_CONNECT_X_DELAYED 0x00008008 +#define SCTP_CONNECT_X_COMPLETE 0x00008009 +/* hidden socket option based sctp_peeloff */ +#define SCTP_PEELOFF 0x0000800a +/* the real worker for sctp_getaddrlen() */ +#define SCTP_GET_ADDR_LEN 0x0000800b +#if defined(__APPLE__) && !defined(__Userspace__) +/* temporary workaround for Apple listen() issue, no args used */ +#define SCTP_LISTEN_FIX 0x0000800c +#endif +#if defined(_WIN32) && !defined(__Userspace__) +/* workaround for Cygwin on Windows: returns the SOCKET handle */ +#define SCTP_GET_HANDLE 0x0000800d +#endif +/* Debug things that need to be purged */ +#define SCTP_SET_INITIAL_DBG_SEQ 0x00009f00 + +/* JRS - Supported congestion control modules for pluggable + * congestion control + */ +/* Standard TCP Congestion Control */ +#define SCTP_CC_RFC2581 0x00000000 +/* High Speed TCP Congestion Control (Floyd) */ +#define SCTP_CC_HSTCP 0x00000001 +/* HTCP Congestion Control */ +#define SCTP_CC_HTCP 0x00000002 +/* RTCC Congestion Control - RFC2581 plus */ +#define SCTP_CC_RTCC 0x00000003 + +#define SCTP_CC_OPT_RTCC_SETMODE 0x00002000 +#define SCTP_CC_OPT_USE_DCCC_ECN 0x00002001 +#define SCTP_CC_OPT_STEADY_STEP 0x00002002 + +#define SCTP_CMT_OFF 0 +#define SCTP_CMT_BASE 1 +#define SCTP_CMT_RPV1 2 +#define SCTP_CMT_RPV2 3 +#define SCTP_CMT_MPTCP 4 +#define SCTP_CMT_MAX SCTP_CMT_MPTCP + +/* RS - Supported stream scheduling modules for pluggable + * stream scheduling + */ +/* Default simple round-robin */ +#define SCTP_SS_DEFAULT 0x00000000 +/* Real round-robin */ +#define SCTP_SS_ROUND_ROBIN 0x00000001 +/* Real round-robin per packet */ +#define SCTP_SS_ROUND_ROBIN_PACKET 0x00000002 +/* Priority */ +#define SCTP_SS_PRIORITY 0x00000003 +/* Fair Bandwidth */ +#define SCTP_SS_FAIR_BANDWIDTH 0x00000004 +/* First-come, first-serve */ +#define SCTP_SS_FIRST_COME 0x00000005 + +/* fragment interleave constants + * setting must be one of these or + * EINVAL returned. + */ +#define SCTP_FRAG_LEVEL_0 0x00000000 +#define SCTP_FRAG_LEVEL_1 0x00000001 +#define SCTP_FRAG_LEVEL_2 0x00000002 + +/* + * user state values + */ +#define SCTP_CLOSED 0x0000 +#define SCTP_BOUND 0x1000 +#define SCTP_LISTEN 0x2000 +#define SCTP_COOKIE_WAIT 0x0002 +#define SCTP_COOKIE_ECHOED 0x0004 +#define SCTP_ESTABLISHED 0x0008 +#define SCTP_SHUTDOWN_SENT 0x0010 +#define SCTP_SHUTDOWN_RECEIVED 0x0020 +#define SCTP_SHUTDOWN_ACK_SENT 0x0040 +#define SCTP_SHUTDOWN_PENDING 0x0080 + +/* + * SCTP operational error codes (user visible) + */ +#define SCTP_CAUSE_NO_ERROR 0x0000 +#define SCTP_CAUSE_INVALID_STREAM 0x0001 +#define SCTP_CAUSE_MISSING_PARAM 0x0002 +#define SCTP_CAUSE_STALE_COOKIE 0x0003 +#define SCTP_CAUSE_OUT_OF_RESC 0x0004 +#define SCTP_CAUSE_UNRESOLVABLE_ADDR 0x0005 +#define SCTP_CAUSE_UNRECOG_CHUNK 0x0006 +#define SCTP_CAUSE_INVALID_PARAM 0x0007 +#define SCTP_CAUSE_UNRECOG_PARAM 0x0008 +#define SCTP_CAUSE_NO_USER_DATA 0x0009 +#define SCTP_CAUSE_COOKIE_IN_SHUTDOWN 0x000a +#define SCTP_CAUSE_RESTART_W_NEWADDR 0x000b +#define SCTP_CAUSE_USER_INITIATED_ABT 0x000c +#define SCTP_CAUSE_PROTOCOL_VIOLATION 0x000d + +/* Error causes from RFC5061 */ +#define SCTP_CAUSE_DELETING_LAST_ADDR 0x00a0 +#define SCTP_CAUSE_RESOURCE_SHORTAGE 0x00a1 +#define SCTP_CAUSE_DELETING_SRC_ADDR 0x00a2 +#define SCTP_CAUSE_ILLEGAL_ASCONF_ACK 0x00a3 +#define SCTP_CAUSE_REQUEST_REFUSED 0x00a4 + +/* Error causes from nat-draft */ +#define SCTP_CAUSE_NAT_COLLIDING_STATE 0x00b0 +#define SCTP_CAUSE_NAT_MISSING_STATE 0x00b1 + +/* Error causes from RFC4895 */ +#define SCTP_CAUSE_UNSUPPORTED_HMACID 0x0105 + +/* + * error cause parameters (user visible) + */ +struct sctp_gen_error_cause { + uint16_t code; + uint16_t length; + uint8_t info[]; +} SCTP_PACKED; + +struct sctp_error_cause { + uint16_t code; + uint16_t length; + /* optional cause-specific info may follow */ +} SCTP_PACKED; + +struct sctp_error_invalid_stream { + struct sctp_error_cause cause; /* code=SCTP_CAUSE_INVALID_STREAM */ + uint16_t stream_id; /* stream id of the DATA in error */ + uint16_t reserved; +} SCTP_PACKED; + +struct sctp_error_missing_param { + struct sctp_error_cause cause; /* code=SCTP_CAUSE_MISSING_PARAM */ + uint32_t num_missing_params; /* number of missing parameters */ + uint16_t type[]; +} SCTP_PACKED; + +struct sctp_error_stale_cookie { + struct sctp_error_cause cause; /* code=SCTP_CAUSE_STALE_COOKIE */ + uint32_t stale_time; /* time in usec of staleness */ +} SCTP_PACKED; + +struct sctp_error_out_of_resource { + struct sctp_error_cause cause; /* code=SCTP_CAUSE_OUT_OF_RESOURCES */ +} SCTP_PACKED; + +struct sctp_error_unresolv_addr { + struct sctp_error_cause cause; /* code=SCTP_CAUSE_UNRESOLVABLE_ADDR */ +} SCTP_PACKED; + +struct sctp_error_unrecognized_chunk { + struct sctp_error_cause cause; /* code=SCTP_CAUSE_UNRECOG_CHUNK */ + struct sctp_chunkhdr ch;/* header from chunk in error */ +} SCTP_PACKED; + +struct sctp_error_no_user_data { + struct sctp_error_cause cause; /* code=SCTP_CAUSE_NO_USER_DATA */ + uint32_t tsn; /* TSN of the empty data chunk */ +} SCTP_PACKED; + +struct sctp_error_auth_invalid_hmac { + struct sctp_error_cause cause; /* code=SCTP_CAUSE_UNSUPPORTED_HMACID */ + uint16_t hmac_id; +} SCTP_PACKED; + +/* + * Main SCTP chunk types we place these here so natd and f/w's in user land + * can find them. + */ +/************0x00 series ***********/ +#define SCTP_DATA 0x00 +#define SCTP_INITIATION 0x01 +#define SCTP_INITIATION_ACK 0x02 +#define SCTP_SELECTIVE_ACK 0x03 +#define SCTP_HEARTBEAT_REQUEST 0x04 +#define SCTP_HEARTBEAT_ACK 0x05 +#define SCTP_ABORT_ASSOCIATION 0x06 +#define SCTP_SHUTDOWN 0x07 +#define SCTP_SHUTDOWN_ACK 0x08 +#define SCTP_OPERATION_ERROR 0x09 +#define SCTP_COOKIE_ECHO 0x0a +#define SCTP_COOKIE_ACK 0x0b +#define SCTP_ECN_ECHO 0x0c +#define SCTP_ECN_CWR 0x0d +#define SCTP_SHUTDOWN_COMPLETE 0x0e +/* RFC4895 */ +#define SCTP_AUTHENTICATION 0x0f +/* EY nr_sack chunk id*/ +#define SCTP_NR_SELECTIVE_ACK 0x10 +/************0x40 series ***********/ +#define SCTP_IDATA 0x40 +/************0x80 series ***********/ +/* RFC5061 */ +#define SCTP_ASCONF_ACK 0x80 +/* draft-ietf-stewart-pktdrpsctp */ +#define SCTP_PACKET_DROPPED 0x81 +/* draft-ietf-stewart-strreset-xxx */ +#define SCTP_STREAM_RESET 0x82 + +/* RFC4820 */ +#define SCTP_PAD_CHUNK 0x84 +/************0xc0 series ***********/ +/* RFC3758 */ +#define SCTP_FORWARD_CUM_TSN 0xc0 +/* RFC5061 */ +#define SCTP_ASCONF 0xc1 +#define SCTP_IFORWARD_CUM_TSN 0xc2 + +/* ABORT and SHUTDOWN COMPLETE FLAG */ +#define SCTP_HAD_NO_TCB 0x01 + +/* Packet dropped flags */ +#define SCTP_FROM_MIDDLE_BOX SCTP_HAD_NO_TCB +#define SCTP_BADCRC 0x02 +#define SCTP_PACKET_TRUNCATED 0x04 + +/* Flag for ECN -CWR */ +#define SCTP_CWR_REDUCE_OVERRIDE 0x01 +#define SCTP_CWR_IN_SAME_WINDOW 0x02 + +#define SCTP_SAT_NETWORK_MIN 400 /* min ms for RTT to set satellite + * time */ +#define SCTP_SAT_NETWORK_BURST_INCR 2 /* how many times to multiply maxburst + * in sat */ + +/* Data Chuck Specific Flags */ +#define SCTP_DATA_FRAG_MASK 0x03 +#define SCTP_DATA_MIDDLE_FRAG 0x00 +#define SCTP_DATA_LAST_FRAG 0x01 +#define SCTP_DATA_FIRST_FRAG 0x02 +#define SCTP_DATA_NOT_FRAG 0x03 +#define SCTP_DATA_UNORDERED 0x04 +#define SCTP_DATA_SACK_IMMEDIATELY 0x08 +/* ECN Nonce: SACK Chunk Specific Flags */ +#define SCTP_SACK_NONCE_SUM 0x01 + +/* CMT DAC algorithm SACK flag */ +#define SCTP_SACK_CMT_DAC 0x80 + +/* + * PCB flags (in sctp_flags bitmask). + * Note the features and flags are meant + * for use by netstat. + */ +#define SCTP_PCB_FLAGS_UDPTYPE 0x00000001 +#define SCTP_PCB_FLAGS_TCPTYPE 0x00000002 +#define SCTP_PCB_FLAGS_BOUNDALL 0x00000004 +#define SCTP_PCB_FLAGS_ACCEPTING 0x00000008 +#define SCTP_PCB_FLAGS_UNBOUND 0x00000010 +#define SCTP_PCB_FLAGS_SND_ITERATOR_UP 0x00000020 +#define SCTP_PCB_FLAGS_CLOSE_IP 0x00040000 +#define SCTP_PCB_FLAGS_WAS_CONNECTED 0x00080000 +#define SCTP_PCB_FLAGS_WAS_ABORTED 0x00100000 +/* TCP model support */ + +#define SCTP_PCB_FLAGS_CONNECTED 0x00200000 +#define SCTP_PCB_FLAGS_IN_TCPPOOL 0x00400000 +#define SCTP_PCB_FLAGS_DONT_WAKE 0x00800000 +#define SCTP_PCB_FLAGS_WAKEOUTPUT 0x01000000 +#define SCTP_PCB_FLAGS_WAKEINPUT 0x02000000 +#define SCTP_PCB_FLAGS_BOUND_V6 0x04000000 +#define SCTP_PCB_FLAGS_BLOCKING_IO 0x08000000 +#define SCTP_PCB_FLAGS_SOCKET_GONE 0x10000000 +#define SCTP_PCB_FLAGS_SOCKET_ALLGONE 0x20000000 +#define SCTP_PCB_FLAGS_SOCKET_CANT_READ 0x40000000 +#if defined(__Userspace__) +#define SCTP_PCB_FLAGS_BOUND_CONN 0x80000000 + +/* flags to copy to new PCB */ +#define SCTP_PCB_COPY_FLAGS (SCTP_PCB_FLAGS_BOUNDALL|\ + SCTP_PCB_FLAGS_WAKEINPUT|\ + SCTP_PCB_FLAGS_BOUND_V6|\ + SCTP_PCB_FLAGS_BOUND_CONN) +#else + +/* flags to copy to new PCB */ +#define SCTP_PCB_COPY_FLAGS (SCTP_PCB_FLAGS_BOUNDALL|\ + SCTP_PCB_FLAGS_WAKEINPUT|\ + SCTP_PCB_FLAGS_BOUND_V6) +#endif + +/* + * PCB Features (in sctp_features bitmask) + */ +#define SCTP_PCB_FLAGS_DO_NOT_PMTUD 0x0000000000000001 +#define SCTP_PCB_FLAGS_EXT_RCVINFO 0x0000000000000002 /* deprecated */ +#define SCTP_PCB_FLAGS_DONOT_HEARTBEAT 0x0000000000000004 +#define SCTP_PCB_FLAGS_FRAG_INTERLEAVE 0x0000000000000008 +#define SCTP_PCB_FLAGS_INTERLEAVE_STRMS 0x0000000000000010 +#define SCTP_PCB_FLAGS_DO_ASCONF 0x0000000000000020 +#define SCTP_PCB_FLAGS_AUTO_ASCONF 0x0000000000000040 +/* socket options */ +#define SCTP_PCB_FLAGS_NODELAY 0x0000000000000100 +#define SCTP_PCB_FLAGS_AUTOCLOSE 0x0000000000000200 +#define SCTP_PCB_FLAGS_RECVDATAIOEVNT 0x0000000000000400 /* deprecated */ +#define SCTP_PCB_FLAGS_RECVASSOCEVNT 0x0000000000000800 +#define SCTP_PCB_FLAGS_RECVPADDREVNT 0x0000000000001000 +#define SCTP_PCB_FLAGS_RECVPEERERR 0x0000000000002000 +#define SCTP_PCB_FLAGS_RECVSENDFAILEVNT 0x0000000000004000 /* deprecated */ +#define SCTP_PCB_FLAGS_RECVSHUTDOWNEVNT 0x0000000000008000 +#define SCTP_PCB_FLAGS_ADAPTATIONEVNT 0x0000000000010000 +#define SCTP_PCB_FLAGS_PDAPIEVNT 0x0000000000020000 +#define SCTP_PCB_FLAGS_AUTHEVNT 0x0000000000040000 +#define SCTP_PCB_FLAGS_STREAM_RESETEVNT 0x0000000000080000 +#define SCTP_PCB_FLAGS_NO_FRAGMENT 0x0000000000100000 +#define SCTP_PCB_FLAGS_EXPLICIT_EOR 0x0000000000400000 +#define SCTP_PCB_FLAGS_NEEDS_MAPPED_V4 0x0000000000800000 +#define SCTP_PCB_FLAGS_MULTIPLE_ASCONFS 0x0000000001000000 +#define SCTP_PCB_FLAGS_PORTREUSE 0x0000000002000000 +#define SCTP_PCB_FLAGS_DRYEVNT 0x0000000004000000 +#define SCTP_PCB_FLAGS_RECVRCVINFO 0x0000000008000000 +#define SCTP_PCB_FLAGS_RECVNXTINFO 0x0000000010000000 +#define SCTP_PCB_FLAGS_ASSOC_RESETEVNT 0x0000000020000000 +#define SCTP_PCB_FLAGS_STREAM_CHANGEEVNT 0x0000000040000000 +#define SCTP_PCB_FLAGS_RECVNSENDFAILEVNT 0x0000000080000000 + +/*- + * mobility_features parameters (by micchie).Note + * these features are applied against the + * sctp_mobility_features flags.. not the sctp_features + * flags. + */ +#define SCTP_MOBILITY_BASE 0x00000001 +#define SCTP_MOBILITY_FASTHANDOFF 0x00000002 +#define SCTP_MOBILITY_PRIM_DELETED 0x00000004 + +/* Smallest PMTU allowed when disabling PMTU discovery */ +#define SCTP_SMALLEST_PMTU 512 +/* Largest PMTU allowed when disabling PMTU discovery */ +#define SCTP_LARGEST_PMTU 65536 + +#if defined(_WIN32) +#pragma pack(pop) +#endif +#undef SCTP_PACKED + +/* This dictates the size of the packet + * collection buffer. This only applies + * if SCTP_PACKET_LOGGING is enabled in + * your config. + */ +#define SCTP_PACKET_LOG_SIZE 65536 + +/* Maximum delays and such a user can set for options that + * take ms. + */ +#define SCTP_MAX_SACK_DELAY 500 /* per RFC4960 */ +#define SCTP_MAX_HB_INTERVAL 14400000 /* 4 hours in ms */ +#define SCTP_MIN_COOKIE_LIFE 1000 /* 1 second in ms */ +#define SCTP_MAX_COOKIE_LIFE 3600000 /* 1 hour in ms */ + +/* Types of logging/KTR tracing that can be enabled via the + * sysctl net.inet.sctp.sctp_logging. You must also enable + * SUBSYS tracing. + * Note that you must have the SCTP option in the kernel + * to enable these as well. + */ +#define SCTP_BLK_LOGGING_ENABLE 0x00000001 +#define SCTP_CWND_MONITOR_ENABLE 0x00000002 +#define SCTP_CWND_LOGGING_ENABLE 0x00000004 +#define SCTP_FLIGHT_LOGGING_ENABLE 0x00000020 +#define SCTP_FR_LOGGING_ENABLE 0x00000040 +#define SCTP_LOCK_LOGGING_ENABLE 0x00000080 +#define SCTP_MAP_LOGGING_ENABLE 0x00000100 +#define SCTP_MBCNT_LOGGING_ENABLE 0x00000200 +#define SCTP_MBUF_LOGGING_ENABLE 0x00000400 +#define SCTP_NAGLE_LOGGING_ENABLE 0x00000800 +#define SCTP_RECV_RWND_LOGGING_ENABLE 0x00001000 +#define SCTP_RTTVAR_LOGGING_ENABLE 0x00002000 +#define SCTP_SACK_LOGGING_ENABLE 0x00004000 +#define SCTP_SACK_RWND_LOGGING_ENABLE 0x00008000 +#define SCTP_SB_LOGGING_ENABLE 0x00010000 +#define SCTP_STR_LOGGING_ENABLE 0x00020000 +#define SCTP_WAKE_LOGGING_ENABLE 0x00040000 +#define SCTP_LOG_MAXBURST_ENABLE 0x00080000 +#define SCTP_LOG_RWND_ENABLE 0x00100000 +#define SCTP_LOG_SACK_ARRIVALS_ENABLE 0x00200000 +#define SCTP_LTRACE_CHUNK_ENABLE 0x00400000 +#define SCTP_LTRACE_ERROR_ENABLE 0x00800000 +#define SCTP_LAST_PACKET_TRACING 0x01000000 +#define SCTP_THRESHOLD_LOGGING 0x02000000 +#define SCTP_LOG_AT_SEND_2_SCTP 0x04000000 +#define SCTP_LOG_AT_SEND_2_OUTQ 0x08000000 +#define SCTP_LOG_TRY_ADVANCE 0x10000000 + +#endif /* !_NETINET_SCTP_H_ */ diff --git a/src/iftraf.c b/src/iftraf.c index a03277402..5d36e6657 100644 --- a/src/iftraf.c +++ b/src/iftraf.c @@ -678,7 +678,8 @@ static int iftraf_pkt_deliver(int af, struct rte_mbuf *mbuf, struct netif_port * struct rte_ipv4_hdr *ip4h = ip4_hdr(mbuf); if (unlikely(ip4h->next_proto_id != IPPROTO_TCP && - ip4h->next_proto_id != IPPROTO_UDP)) { + ip4h->next_proto_id != IPPROTO_UDP && + ip4h->next_proto_id != IPPROTO_SCTP)) { RTE_LOG(DEBUG, IFTRAF, "%s: unspported proto[core: %d, proto: %d]\n", __func__, cid, ip4h->next_proto_id); @@ -740,7 +741,8 @@ static int iftraf_pkt_deliver(int af, struct rte_mbuf *mbuf, struct netif_port * uint8_t ip6nxt = ip6h->ip6_nxt; if (unlikely(ip6nxt != IPPROTO_TCP && - ip6nxt != IPPROTO_UDP)) { + ip6nxt != IPPROTO_UDP && + ip6nxt != IPPROTO_SCTP)) { RTE_LOG(DEBUG, IFTRAF, "%s: unspported proto[core: %d, proto: %d]\n", __func__, cid, ip6nxt); diff --git a/src/ipvs/ip_vs_conn.c b/src/ipvs/ip_vs_conn.c index e8deb0576..f40bfb470 100644 --- a/src/ipvs/ip_vs_conn.c +++ b/src/ipvs/ip_vs_conn.c @@ -30,6 +30,7 @@ #include "ipvs/synproxy.h" #include "ipvs/proto_tcp.h" #include "ipvs/proto_udp.h" +#include "ipvs/proto_sctp.h" #include "ipvs/proto_icmp.h" #include "parser/parser.h" #include "ctrl.h" @@ -517,7 +518,9 @@ void dp_vs_conn_set_timeout(struct dp_vs_conn *conn, struct dp_vs_proto *pp) /* set proper timeout */ if ((conn->proto == IPPROTO_TCP && conn->state == DPVS_TCP_S_ESTABLISHED) - || conn->proto == IPPROTO_UDP) { + || conn->proto == IPPROTO_UDP + || (conn->proto == IPPROTO_SCTP && + conn->state == DPVS_SCTP_S_ESTABLISHED)) { conn_timeout = dp_vs_conn_get_timeout(conn); if (conn_timeout > 0) { @@ -1308,6 +1311,39 @@ static inline char* get_conn_state_name(uint16_t proto, uint16_t state) break; } break; + case IPPROTO_SCTP: + switch (state) { + case DPVS_SCTP_S_NONE: + return "SCTP_NONE"; + case DPVS_SCTP_S_INIT1: + return "SCTP_INIT1"; + case DPVS_SCTP_S_INIT: + return "SCTP_INIT"; + case DPVS_SCTP_S_COOKIE_SENT: + return "SCTP_COOKIE_SENT"; + case DPVS_SCTP_S_COOKIE_REPLIED: + return "SCTP_COOKIE_REPLIED"; + case DPVS_SCTP_S_COOKIE_WAIT: + return "SCTP_COOKIE_WAIT"; + case DPVS_SCTP_S_COOKIE: + return "SCTP_COOKIE"; + case DPVS_SCTP_S_COOKIE_ECHOED: + return "SCTP_COOKIE_ECHOED"; + case DPVS_SCTP_S_ESTABLISHED: + return "SCTP_ESTABLISHED"; + case DPVS_SCTP_S_SHUTDOWN_SENT: + return "SCTP_SHUTDOWN_SENT"; + case DPVS_SCTP_S_SHUTDOWN_RECEIVED: + return "SCTP_SHUTDOWN_RECEIVED"; + case DPVS_SCTP_S_SHUTDOWN_ACK_SENT: + return "SCTP_SHUTDOWN_ACK_SENT"; + case DPVS_SCTP_S_REJECTED: + return "SCTP_REJECTED"; + case DPVS_SCTP_S_CLOSED: + return "SCTP_CLOSED"; + default: + return "SCTP_UNKNOWN"; + } case IPPROTO_ICMP: case IPPROTO_ICMPV6: switch (state) { diff --git a/src/ipvs/ip_vs_dest.c b/src/ipvs/ip_vs_dest.c index 8564c6ebe..a3a01fbe9 100644 --- a/src/ipvs/ip_vs_dest.c +++ b/src/ipvs/ip_vs_dest.c @@ -394,7 +394,7 @@ static void dest_inhibit_logging(const struct dp_vs_dest *dest, const char *msg) RTE_LOG(INFO, SERVICE, "[cid %02d, %s, svc %s:%d, rs %s:%d, weight %d, inhibited %s," " down_notice_recvd %d, inhibit_duration %ds, origin_weight %d] %s\n", cid, - dest->proto == IPPROTO_TCP ? "tcp" : "udp", + dest->proto == IPPROTO_TCP ? "tcp" : IPPROTO_UDP ? "udp" : "sctp", inet_ntop(dest->svc->af, &dest->svc->addr, str_vaddr, sizeof(str_vaddr)) ? str_vaddr : "::", ntohs(dest->svc->port), inet_ntop(dest->af, &dest->addr, str_daddr, sizeof(str_daddr)) ? str_daddr : "::", @@ -409,7 +409,7 @@ static void dest_inhibit_logging(const struct dp_vs_dest *dest, const char *msg) } else { RTE_LOG(DEBUG, SERVICE, "[cid %02d, %s, svc %s:%d, rs %s:%d, weight %d, inhibited %s, warm_up_count %d] %s\n", cid, - dest->proto == IPPROTO_TCP ? "tcp" : "udp", + dest->proto == IPPROTO_TCP ? "tcp" : IPPROTO_UDP ? "udp" : "sctp", inet_ntop(dest->svc->af, &dest->svc->addr, str_vaddr, sizeof(str_vaddr)) ? str_vaddr : "::", ntohs(dest->svc->port), inet_ntop(dest->af, &dest->addr, str_daddr, sizeof(str_daddr)) ? str_daddr : "::", diff --git a/src/ipvs/ip_vs_laddr.c b/src/ipvs/ip_vs_laddr.c index 4f15005a9..fa04e4258 100644 --- a/src/ipvs/ip_vs_laddr.c +++ b/src/ipvs/ip_vs_laddr.c @@ -175,7 +175,8 @@ int dp_vs_laddr_bind(struct dp_vs_conn *conn, struct dp_vs_service *svc) if (!conn || !conn->dest || !svc) return EDPVS_INVAL; - if (svc->proto != IPPROTO_TCP && svc->proto != IPPROTO_UDP) + if (svc->proto != IPPROTO_TCP && svc->proto != IPPROTO_UDP && + svc->proto != IPPROTO_SCTP) return EDPVS_NOTSUPP; if (dp_vs_conn_is_template(conn)) return EDPVS_OK; diff --git a/src/ipvs/ip_vs_nat64.c b/src/ipvs/ip_vs_nat64.c index e9a827b60..b0ff237a6 100644 --- a/src/ipvs/ip_vs_nat64.c +++ b/src/ipvs/ip_vs_nat64.c @@ -34,6 +34,7 @@ int mbuf_6to4(struct rte_mbuf *mbuf, */ if (ip6h->ip6_nxt != IPPROTO_TCP && ip6h->ip6_nxt != IPPROTO_UDP && + ip6h->ip6_nxt != IPPROTO_SCTP && ip6h->ip6_nxt != IPPROTO_ICMPV6 && ip6h->ip6_nxt != IPPROTO_OPT) { return EDPVS_NOTSUPP; diff --git a/src/ipvs/ip_vs_proto.c b/src/ipvs/ip_vs_proto.c index bb5baa5ff..9d7f1e422 100644 --- a/src/ipvs/ip_vs_proto.c +++ b/src/ipvs/ip_vs_proto.c @@ -71,6 +71,7 @@ struct dp_vs_proto *dp_vs_proto_lookup(uint8_t proto) extern struct dp_vs_proto dp_vs_proto_udp; extern struct dp_vs_proto dp_vs_proto_tcp; +extern struct dp_vs_proto dp_vs_proto_sctp; extern struct dp_vs_proto dp_vs_proto_icmp; extern struct dp_vs_proto dp_vs_proto_icmp6; @@ -88,6 +89,11 @@ int dp_vs_proto_init(void) goto tcp_error; } + if ((err = proto_register(&dp_vs_proto_sctp)) != EDPVS_OK) { + RTE_LOG(ERR, IPVS, "%s: fail to register SCTP\n", __func__); + goto sctp_error; + } + if ((err = proto_register(&dp_vs_proto_icmp6)) != EDPVS_OK) { RTE_LOG(ERR, IPVS, "%s: fail to register ICMPV6\n", __func__); goto icmp6_error; @@ -103,6 +109,8 @@ int dp_vs_proto_init(void) icmp_error: proto_unregister(&dp_vs_proto_icmp6); icmp6_error: + proto_unregister(&dp_vs_proto_sctp); +sctp_error: proto_unregister(&dp_vs_proto_tcp); tcp_error: proto_unregister(&dp_vs_proto_udp); @@ -117,6 +125,9 @@ int dp_vs_proto_term(void) if (proto_unregister(&dp_vs_proto_icmp6) != EDPVS_OK) RTE_LOG(ERR, IPVS, "%s: fail to unregister ICMPV6\n", __func__); + if (proto_unregister(&dp_vs_proto_sctp) != EDPVS_OK) + RTE_LOG(ERR, IPVS, "%s: fail to unregister SCTP\n", __func__); + if (proto_unregister(&dp_vs_proto_tcp) != EDPVS_OK) RTE_LOG(ERR, IPVS, "%s: fail to unregister TCP\n", __func__); diff --git a/src/ipvs/ip_vs_proto_sctp.c b/src/ipvs/ip_vs_proto_sctp.c new file mode 100644 index 000000000..28b067ae1 --- /dev/null +++ b/src/ipvs/ip_vs_proto_sctp.c @@ -0,0 +1,558 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "conf/common.h" +#include "dpdk.h" +#include "mbuf.h" +#include "ipv6.h" +#include "route6.h" +#include "neigh.h" +#include "ipvs/ipvs.h" +#include "ipvs/proto.h" +#include "ipvs/proto_sctp.h" +#include "ipvs/conn.h" +#include "ipvs/service.h" +#include "ipvs/dest.h" +#include "ipvs/synproxy.h" +#include "ipvs/blklst.h" +#include "ipvs/whtlst.h" +#include "parser/parser.h" +#include "rte_hash_crc.h" + +/* + * Compute the SCTP checksum in network byte order for a given mbuf chain m + * which contains an SCTP packet starting at offset. + * Since this function is also called by ipfw, don't assume that + * it is compiled on a kernel with SCTP support. + */ +static inline uint32_t sctp_calculate_cksum(struct rte_mbuf *mbuf, + int32_t offset) +{ + int len; + uint32_t _old, _new; + + len = mbuf->data_len; + + struct sctphdr *sh = + rte_pktmbuf_mtod_offset(mbuf, struct sctphdr *, offset); + + _old = sh->checksum; + + sh->checksum = 0; + + _new = ~rte_hash_crc(rte_pktmbuf_mtod_offset(mbuf, const void *, + offset), + len - offset, ~(uint32_t)0); + + sh->checksum = _old; + + return _new; +} + +static int sctp_csum_check(struct dp_vs_proto *proto, int af, + struct rte_mbuf *mbuf); + +static struct dp_vs_conn *sctp_conn_lookup(struct dp_vs_proto *proto, + const struct dp_vs_iphdr *iph, + struct rte_mbuf *mbuf, int *direct, + bool reverse, bool *drop, + lcoreid_t *peer_cid) +{ + struct sctphdr *sh, _sctph; + struct sctp_chunkhdr *sch, _schunkh; + struct dp_vs_conn *conn; + assert(proto && iph && mbuf); + + sh = mbuf_header_pointer(mbuf, iph->len, sizeof(_sctph), &_sctph); + if (unlikely(!sh)) + return NULL; + + sch = mbuf_header_pointer(mbuf, iph->len + sizeof(_sctph), + sizeof(_schunkh), &_schunkh); + if (unlikely(!sch)) + return NULL; + + if (dp_vs_blklst_lookup(iph->af, iph->proto, &iph->daddr, sh->dest_port, + &iph->saddr)) { + *drop = true; + return NULL; + } + + if (!dp_vs_whtlst_allow(iph->af, iph->proto, &iph->daddr, sh->dest_port, + &iph->saddr)) { + *drop = true; + return NULL; + } + + conn = dp_vs_conn_get(iph->af, iph->proto, &iph->saddr, &iph->daddr, + sh->src_port, sh->dest_port, direct, reverse); + + /* + * L2 confirm neighbour + * pkt in from client confirm neighbour to client + * pkt out from rs confirm neighbour to rs + */ + if (conn != NULL) { + if ((*direct == DPVS_CONN_DIR_INBOUND) && conn->out_dev && + (!inet_is_addr_any(tuplehash_in(conn).af, + &conn->out_nexthop))) { + neigh_confirm(tuplehash_in(conn).af, &conn->out_nexthop, + conn->out_dev); + } else if ((*direct == DPVS_CONN_DIR_OUTBOUND) && + conn->in_dev && + (!inet_is_addr_any(tuplehash_out(conn).af, + &conn->in_nexthop))) { + neigh_confirm(tuplehash_out(conn).af, &conn->in_nexthop, + conn->in_dev); + } + } else { + struct dp_vs_redirect *r; + + r = dp_vs_redirect_get(iph->af, iph->proto, &iph->saddr, + &iph->daddr, sh->src_port, + sh->dest_port); + if (r) { + *peer_cid = r->cid; + } + } + + return conn; +} + +static int sctp_conn_schedule(struct dp_vs_proto *proto, + const struct dp_vs_iphdr *iph, + struct rte_mbuf *mbuf, struct dp_vs_conn **conn, + int *verdict) +{ + struct sctphdr *sh, _sctph; + struct dp_vs_service *svc; + + assert(proto && iph && mbuf && conn && verdict); + + sh = mbuf_header_pointer(mbuf, iph->len, sizeof(_sctph), &_sctph); + if (unlikely(!sh)) { + *verdict = INET_DROP; + return EDPVS_INVPKT; + } + + svc = dp_vs_service_lookup(iph->af, iph->proto, &iph->daddr, + sh->dest_port, 0, mbuf, NULL, rte_lcore_id()); + if (!svc) { + *verdict = INET_ACCEPT; + return EDPVS_NOSERV; + } + + *conn = dp_vs_schedule(svc, iph, mbuf, false); + if (!*conn) { + *verdict = INET_DROP; + return EDPVS_RESOURCE; + } + + return EDPVS_OK; +} + +static void sctp_nat_csum(struct rte_mbuf *mbuf, struct sctphdr *sctph, + unsigned int offset) +{ + sctph->checksum = sctp_calculate_cksum(mbuf, offset); +} + +static int sctp_fnat_in_handler(struct dp_vs_proto *proto, + struct dp_vs_conn *conn, struct rte_mbuf *mbuf) +{ + struct sctphdr *sh; + + /* af/mbuf may be changed for nat64 which in af is ipv6 and out is ipv4 */ + int af = tuplehash_out(conn).af; + int iphdrlen = ((AF_INET6 == af) ? ip6_hdrlen(mbuf) : ip4_hdrlen(mbuf)); + + if (mbuf_may_pull(mbuf, iphdrlen + sizeof(*sh)) != 0) + return EDPVS_INVPKT; + + sh = rte_pktmbuf_mtod_offset(mbuf, struct sctphdr *, iphdrlen); + + /* Some checks before mangling */ + if (sctp_csum_check(proto, af, mbuf)) + return EDPVS_INVAL; + + /* L4 translation */ + sh->src_port = conn->lport; + sh->dest_port = conn->dport; + + sctp_nat_csum(mbuf, sh, iphdrlen); + + return EDPVS_OK; +} + +static int sctp_fnat_out_handler(struct dp_vs_proto *proto, + struct dp_vs_conn *conn, struct rte_mbuf *mbuf) +{ + struct sctphdr *sh; + + /* af/mbuf may be changed for nat64 which in af is ipv6 and out is ipv4 */ + int af = tuplehash_in(conn).af; + int iphdrlen = ((AF_INET6 == af) ? ip6_hdrlen(mbuf) : ip4_hdrlen(mbuf)); + + if (mbuf_may_pull(mbuf, iphdrlen + sizeof(*sh)) != 0) + return EDPVS_INVPKT; + + sh = rte_pktmbuf_mtod_offset(mbuf, struct sctphdr *, iphdrlen); + + /* Some checks before mangling */ + if (sctp_csum_check(proto, af, mbuf)) + return EDPVS_INVAL; + + /* L4 translation */ + sh->src_port = conn->vport; + sh->dest_port = conn->cport; + + sctp_nat_csum(mbuf, sh, iphdrlen); + + return EDPVS_OK; +} + +static int sctp_nat_in_handler(struct dp_vs_proto *proto, + struct dp_vs_conn *conn, struct rte_mbuf *mbuf) +{ + struct sctphdr *sh; + int af = conn->af; + int iphdrlen = ((AF_INET6 == af) ? ip6_hdrlen(mbuf) : ip4_hdrlen(mbuf)); + + if (mbuf_may_pull(mbuf, iphdrlen + sizeof(*sh)) != 0) + return EDPVS_INVPKT; + + sh = rte_pktmbuf_mtod_offset(mbuf, struct sctphdr *, iphdrlen); + + /* Some checks before mangling */ + if (sctp_csum_check(proto, af, mbuf)) + return EDPVS_INVAL; + + /* Only update csum if we really have to */ + sh->dest_port = conn->dport; + sctp_nat_csum(mbuf, sh, iphdrlen); + + return EDPVS_OK; +} + +static int sctp_nat_out_handler(struct dp_vs_proto *proto, + struct dp_vs_conn *conn, struct rte_mbuf *mbuf) +{ + struct sctphdr *sh; + int af = conn->af; + int iphdrlen = ((AF_INET6 == af) ? ip6_hdrlen(mbuf) : ip4_hdrlen(mbuf)); + + if (mbuf_may_pull(mbuf, iphdrlen + sizeof(*sh)) != 0) + return EDPVS_INVPKT; + + sh = rte_pktmbuf_mtod_offset(mbuf, struct sctphdr *, iphdrlen); + + /* Some checks before mangling */ + if (sctp_csum_check(proto, af, mbuf)) + return EDPVS_INVAL; + + /* Only update csum if we really have to */ + sh->src_port = conn->vport; + sctp_nat_csum(mbuf, sh, iphdrlen); + + return EDPVS_OK; +} + +static int sctp_csum_check(struct dp_vs_proto *proto, int af, + struct rte_mbuf *mbuf) +{ + struct sctphdr *sh; + uint32_t cmp, val; + int iphdrlen = ((AF_INET6 == af) ? ip6_hdrlen(mbuf) : ip4_hdrlen(mbuf)); + + sh = rte_pktmbuf_mtod_offset(mbuf, struct sctphdr *, iphdrlen); + cmp = sh->checksum; + val = sctp_calculate_cksum(mbuf, iphdrlen); + + if (val != cmp) { + /* CRC failure, dump it. */ + RTE_LOG(WARNING, IPVS, "Failed checksum for %d %s %p!\n", af, + proto->name, mbuf); + return EDPVS_INVAL; + } + return EDPVS_OK; +} + +/* RFC 2960, 3.2 Chunk Field Descriptions */ +static __u8 sctp_events[] = { + [SCTP_DATA] = DPVS_SCTP_DATA, + [SCTP_INITIATION] = DPVS_SCTP_INIT, + [SCTP_INITIATION_ACK] = DPVS_SCTP_INIT_ACK, + [SCTP_SELECTIVE_ACK] = DPVS_SCTP_DATA, + [SCTP_HEARTBEAT_REQUEST] = DPVS_SCTP_DATA, + [SCTP_HEARTBEAT_ACK] = DPVS_SCTP_DATA, + [SCTP_ABORT_ASSOCIATION] = DPVS_SCTP_ABORT, + [SCTP_SHUTDOWN] = DPVS_SCTP_SHUTDOWN, + [SCTP_SHUTDOWN_ACK] = DPVS_SCTP_SHUTDOWN_ACK, + [SCTP_OPERATION_ERROR] = DPVS_SCTP_ERROR, + [SCTP_COOKIE_ECHO] = DPVS_SCTP_COOKIE_ECHO, + [SCTP_COOKIE_ACK] = DPVS_SCTP_COOKIE_ACK, + [SCTP_ECN_ECHO] = DPVS_SCTP_DATA, + [SCTP_ECN_CWR] = DPVS_SCTP_DATA, + [SCTP_SHUTDOWN_COMPLETE] = DPVS_SCTP_SHUTDOWN_COMPLETE, +}; + +/* SCTP States: + * See RFC 2960, 4. SCTP Association State Diagram + * + * New states (not in diagram): + * - INIT1 state: use shorter timeout for dropped INIT packets + * - REJECTED state: use shorter timeout if INIT is rejected with ABORT + * - INIT, COOKIE_SENT, COOKIE_REPLIED, COOKIE states: for better debugging + * + * The states are as seen in real server. In the diagram, INIT1, INIT, + * COOKIE_SENT and COOKIE_REPLIED processing happens in CLOSED state. + * + * States as per packets from client (C) and server (S): + * + * Setup of client connection: + * DPVS_SCTP_S_INIT1: First C:INIT sent, wait for S:INIT-ACK + * DPVS_SCTP_S_INIT: Next C:INIT sent, wait for S:INIT-ACK + * DPVS_SCTP_S_COOKIE_SENT: S:INIT-ACK sent, wait for C:COOKIE-ECHO + * DPVS_SCTP_S_COOKIE_REPLIED: C:COOKIE-ECHO sent, wait for S:COOKIE-ACK + * + * Setup of server connection: + * DPVS_SCTP_S_COOKIE_WAIT: S:INIT sent, wait for C:INIT-ACK + * DPVS_SCTP_S_COOKIE: C:INIT-ACK sent, wait for S:COOKIE-ECHO + * DPVS_SCTP_S_COOKIE_ECHOED: S:COOKIE-ECHO sent, wait for C:COOKIE-ACK + */ + +#define sNO DPVS_SCTP_S_NONE +#define sI1 DPVS_SCTP_S_INIT1 +#define sIN DPVS_SCTP_S_INIT +#define sCS DPVS_SCTP_S_COOKIE_SENT +#define sCR DPVS_SCTP_S_COOKIE_REPLIED +#define sCW DPVS_SCTP_S_COOKIE_WAIT +#define sCO DPVS_SCTP_S_COOKIE +#define sCE DPVS_SCTP_S_COOKIE_ECHOED +#define sES DPVS_SCTP_S_ESTABLISHED +#define sSS DPVS_SCTP_S_SHUTDOWN_SENT +#define sSR DPVS_SCTP_S_SHUTDOWN_RECEIVED +#define sSA DPVS_SCTP_S_SHUTDOWN_ACK_SENT +#define sRJ DPVS_SCTP_S_REJECTED +#define sCL DPVS_SCTP_S_CLOSED + +static const __u8 sctp_states[DPVS_DIR_LAST][DPVS_SCTP_EVENT_LAST][DPVS_SCTP_S_LAST] = { + { /* INPUT */ +/* sNO, sI1, sIN, sCS, sCR, sCW, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL*/ +/* d */ { sES, sI1, sIN, sCS, sCR, sCW, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* i */ { sI1, sIN, sIN, sCS, sCR, sCW, sCO, sCE, sES, sSS, sSR, sSA, sIN, sIN }, +/* i_a */ { sCW, sCW, sCW, sCS, sCR, sCO, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* c_e */ { sCR, sIN, sIN, sCR, sCR, sCW, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* c_a */ { sES, sI1, sIN, sCS, sCR, sCW, sCO, sES, sES, sSS, sSR, sSA, sRJ, sCL }, +/* s */ { sSR, sI1, sIN, sCS, sCR, sCW, sCO, sCE, sSR, sSS, sSR, sSA, sRJ, sCL }, +/* s_a */ { sCL, sIN, sIN, sCS, sCR, sCW, sCO, sCE, sES, sCL, sSR, sCL, sRJ, sCL }, +/* s_c */ { sCL, sCL, sCL, sCS, sCR, sCW, sCO, sCE, sES, sSS, sSR, sCL, sRJ, sCL }, +/* err */ { sCL, sI1, sIN, sCS, sCR, sCW, sCO, sCL, sES, sSS, sSR, sSA, sRJ, sCL }, +/* ab */ { sCL, sCL, sCL, sCL, sCL, sRJ, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL }, + }, + { /* OUTPUT */ +/* sNO, sI1, sIN, sCS, sCR, sCW, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL*/ +/* d */ { sES, sI1, sIN, sCS, sCR, sCW, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* i */ { sCW, sCW, sCW, sCW, sCW, sCW, sCW, sCW, sES, sCW, sCW, sCW, sCW, sCW }, +/* i_a */ { sCS, sCS, sCS, sCS, sCR, sCW, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* c_e */ { sCE, sCE, sCE, sCE, sCE, sCE, sCE, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* c_a */ { sES, sES, sES, sES, sES, sES, sES, sES, sES, sSS, sSR, sSA, sRJ, sCL }, +/* s */ { sSS, sSS, sSS, sSS, sSS, sSS, sSS, sSS, sSS, sSS, sSR, sSA, sRJ, sCL }, +/* s_a */ { sSA, sSA, sSA, sSA, sSA, sCW, sCO, sCE, sES, sSA, sSA, sSA, sRJ, sCL }, +/* s_c */ { sCL, sI1, sIN, sCS, sCR, sCW, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* err */ { sCL, sCL, sCL, sCL, sCL, sCW, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* ab */ { sCL, sRJ, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL }, + }, + { /* INPUT-ONLY */ +/* sNO, sI1, sIN, sCS, sCR, sCW, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL*/ +/* d */ { sES, sI1, sIN, sCS, sCR, sES, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* i */ { sI1, sIN, sIN, sIN, sIN, sIN, sCO, sCE, sES, sSS, sSR, sSA, sIN, sIN }, +/* i_a */ { sCE, sCE, sCE, sCE, sCE, sCE, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* c_e */ { sES, sES, sES, sES, sES, sES, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* c_a */ { sES, sI1, sIN, sES, sES, sCW, sES, sES, sES, sSS, sSR, sSA, sRJ, sCL }, +/* s */ { sSR, sI1, sIN, sCS, sCR, sCW, sCO, sCE, sSR, sSS, sSR, sSA, sRJ, sCL }, +/* s_a */ { sCL, sIN, sIN, sCS, sCR, sCW, sCO, sCE, sCL, sCL, sSR, sCL, sRJ, sCL }, +/* s_c */ { sCL, sCL, sCL, sCL, sCL, sCW, sCO, sCE, sES, sSS, sCL, sCL, sRJ, sCL }, +/* err */ { sCL, sI1, sIN, sCS, sCR, sCW, sCO, sCE, sES, sSS, sSR, sSA, sRJ, sCL }, +/* ab */ { sCL, sCL, sCL, sCL, sCL, sRJ, sCL, sCL, sCL, sCL, sCL, sCL, sCL, sCL }, + }, +}; + +#define DPVS_SCTP_MAX_RTO (60 + 1) + +/* Timeout table[state] */ +static int sctp_timeouts[DPVS_SCTP_S_LAST + 1] = { + [DPVS_SCTP_S_NONE] = 2, + [DPVS_SCTP_S_INIT1] = (0 + 3 + 1), + [DPVS_SCTP_S_INIT] = DPVS_SCTP_MAX_RTO, + [DPVS_SCTP_S_COOKIE_SENT] = DPVS_SCTP_MAX_RTO, + [DPVS_SCTP_S_COOKIE_REPLIED] = DPVS_SCTP_MAX_RTO, + [DPVS_SCTP_S_COOKIE_WAIT] = DPVS_SCTP_MAX_RTO, + [DPVS_SCTP_S_COOKIE] = DPVS_SCTP_MAX_RTO, + [DPVS_SCTP_S_COOKIE_ECHOED] = DPVS_SCTP_MAX_RTO, + [DPVS_SCTP_S_ESTABLISHED] = 15 * 60, + [DPVS_SCTP_S_SHUTDOWN_SENT] = DPVS_SCTP_MAX_RTO, + [DPVS_SCTP_S_SHUTDOWN_RECEIVED] = DPVS_SCTP_MAX_RTO, + [DPVS_SCTP_S_SHUTDOWN_ACK_SENT] = DPVS_SCTP_MAX_RTO, + [DPVS_SCTP_S_REJECTED] = (0 + 3 + 1), + [DPVS_SCTP_S_CLOSED] = DPVS_SCTP_MAX_RTO, + [DPVS_SCTP_S_LAST] = 2, +}; + +static const char *sctp_state_name_table[DPVS_SCTP_S_LAST + 1] = { + [DPVS_SCTP_S_NONE] = "NONE", + [DPVS_SCTP_S_INIT1] = "INIT1", + [DPVS_SCTP_S_INIT] = "INIT", + [DPVS_SCTP_S_COOKIE_SENT] = "C-SENT", + [DPVS_SCTP_S_COOKIE_REPLIED] = "C-REPLIED", + [DPVS_SCTP_S_COOKIE_WAIT] = "C-WAIT", + [DPVS_SCTP_S_COOKIE] = "COOKIE", + [DPVS_SCTP_S_COOKIE_ECHOED] = "C-ECHOED", + [DPVS_SCTP_S_ESTABLISHED] = "ESTABLISHED", + [DPVS_SCTP_S_SHUTDOWN_SENT] = "S-SENT", + [DPVS_SCTP_S_SHUTDOWN_RECEIVED] = "S-RECEIVED", + [DPVS_SCTP_S_SHUTDOWN_ACK_SENT] = "S-ACK-SENT", + [DPVS_SCTP_S_REJECTED] = "REJECTED", + [DPVS_SCTP_S_CLOSED] = "CLOSED", + [DPVS_SCTP_S_LAST] = "BUG!", +}; + +static const char *sctp_state_name(int state) +{ + if (state >= DPVS_SCTP_S_LAST) + return "ERR!"; + if (sctp_state_name_table[state]) + return sctp_state_name_table[state]; + return "?"; +} + +static int sctp_state_trans(struct dp_vs_proto *proto, struct dp_vs_conn *conn, + struct rte_mbuf *mbuf, int dir) +{ + struct sctp_chunkhdr _sctpch, *sch; + unsigned char chunk_type; + int event, next_state; + int iphdrlen, cofs; + assert(proto && conn && mbuf); + + iphdrlen = + ((AF_INET6 == conn->af) ? ip6_hdrlen(mbuf) : ip4_hdrlen(mbuf)); + + cofs = iphdrlen + sizeof(struct sctphdr); + sch = mbuf_header_pointer(mbuf, cofs, sizeof(_sctpch), &_sctpch); + if (!sch) + return EDPVS_INVPKT; + + chunk_type = sch->chunk_type; + /* + * Section 3: Multiple chunks can be bundled into one SCTP packet + * up to the MTU size, except for the INIT, INIT ACK, and + * SHUTDOWN COMPLETE chunks. These chunks MUST NOT be bundled with + * any other chunk in a packet. + * + * Section 3.3.7: DATA chunks MUST NOT be bundled with ABORT. Control + * chunks (except for INIT, INIT ACK, and SHUTDOWN COMPLETE) MAY be + * bundled with an ABORT, but they MUST be placed before the ABORT + * in the SCTP packet or they will be ignored by the receiver. + */ + if ((sch->chunk_type == SCTP_COOKIE_ECHO) || + (sch->chunk_type == SCTP_COOKIE_ACK)) { + int clen = ntohs(sch->chunk_length); + + if (clen >= sizeof(_sctpch)) { + sch = mbuf_header_pointer(mbuf, + cofs + RTE_ALIGN(clen, 4), + sizeof(_sctpch), &_sctpch); + if (sch && sch->chunk_type == SCTP_ABORT_ASSOCIATION) + chunk_type = sch->chunk_type; + } + } + + event = (chunk_type < sizeof(sctp_events)) ? sctp_events[chunk_type] : + DPVS_SCTP_DATA; + + next_state = sctp_states[dir][event][conn->state]; + + if (next_state != conn->state) { + struct dp_vs_dest *dest = conn->dest; + +#ifdef CONFIG_DPVS_IPVS_DEBUG + RTE_LOG(DEBUG, IPVS, + "%s %s %X:%d->" + "%X:%d state: %s->%s conn->refcnt:%d\n", + proto->name, + ((dir == DPVS_CONN_DIR_OUTBOUND) ? "output " : + "input "), + inet_addr_fold(conn->af, &conn->caddr), + ntohs(conn->dport), + inet_addr_fold(conn->af, &conn->caddr), + ntohs(conn->cport), sctp_state_name(conn->state), + sctp_state_name(next_state), + rte_atomic32_read(&conn->refcnt)); +#endif + if (dest) { + if (!(conn->flags & DPVS_CONN_F_INACTIVE) && + (next_state != DPVS_SCTP_S_ESTABLISHED)) { + rte_atomic32_dec(&dest->actconns); + rte_atomic32_inc(&dest->inactconns); + conn->flags |= DPVS_CONN_F_INACTIVE; + } else if ((conn->flags & DPVS_CONN_F_INACTIVE) && + (next_state == DPVS_SCTP_S_ESTABLISHED)) { + rte_atomic32_inc(&dest->actconns); + rte_atomic32_dec(&dest->inactconns); + conn->flags &= ~DPVS_CONN_F_INACTIVE; + } + } + conn->old_state = conn->state; + conn->state = next_state; + } + dp_vs_conn_set_timeout(conn, proto); + return EDPVS_OK; +} + +static int sctp_conn_expire(struct dp_vs_proto *proto, struct dp_vs_conn *conn) +{ + if (conn && conn->prot_data) + rte_free(conn->prot_data); + + return EDPVS_OK; +} + +static int sctp_conn_expire_quiescent(struct dp_vs_conn *conn) +{ + dp_vs_conn_expire_now(conn); + + return EDPVS_OK; +} + +static int sctp_init(struct dp_vs_proto *proto) +{ + if (!proto) + return EDPVS_INVAL; + + proto->timeout_table = sctp_timeouts; + + return EDPVS_OK; +} + +static int sctp_exit(struct dp_vs_proto *proto) +{ + return EDPVS_OK; +} + +struct dp_vs_proto dp_vs_proto_sctp = { + .name = "SCTP", + .proto = IPPROTO_SCTP, + .init = sctp_init, + .exit = sctp_exit, + .conn_sched = sctp_conn_schedule, + .conn_lookup = sctp_conn_lookup, + .conn_expire = sctp_conn_expire, + .conn_expire_quiescent = sctp_conn_expire_quiescent, + .nat_in_handler = sctp_nat_in_handler, + .nat_out_handler = sctp_nat_out_handler, + .fnat_in_handler = sctp_fnat_in_handler, + .fnat_out_handler = sctp_fnat_out_handler, + .snat_in_handler = sctp_nat_in_handler, + .snat_out_handler = sctp_nat_out_handler, + .state_trans = sctp_state_trans, + .state_name = sctp_state_name, +}; diff --git a/src/ipvs/ip_vs_service.c b/src/ipvs/ip_vs_service.c index 44907dcd0..974f8527c 100644 --- a/src/ipvs/ip_vs_service.c +++ b/src/ipvs/ip_vs_service.c @@ -913,7 +913,8 @@ static int dp_vs_service_set(sockoptid_t opt, const void *user, size_t len) } if (usvc.proto != IPPROTO_TCP && usvc.proto != IPPROTO_UDP && - usvc.proto != IPPROTO_ICMP && usvc.proto != IPPROTO_ICMPV6) { + usvc.proto != IPPROTO_SCTP && usvc.proto != IPPROTO_ICMP && + usvc.proto != IPPROTO_ICMPV6) { RTE_LOG(ERR, SERVICE, "%s: protocol not support.\n", __func__); return EDPVS_INVAL; } diff --git a/src/tc/cls_match.c b/src/tc/cls_match.c index ab7ec357d..8f5bcf6ec 100644 --- a/src/tc/cls_match.c +++ b/src/tc/cls_match.c @@ -25,6 +25,7 @@ #include #include #include +#include "sctp/sctp.h" #include "netif.h" #include "vlan.h" #include "tc/tc.h" @@ -54,6 +55,7 @@ static int match_classify(struct tc_cls *cls, struct rte_mbuf *mbuf, struct ip6_hdr *ip6h = NULL; struct tcphdr *th; struct udphdr *uh; + struct sctphdr *sh; uint8_t l4_proto = 0; int offset = sizeof(*eh); __be16 pkt_type = eh->ether_type; @@ -175,6 +177,17 @@ static int match_classify(struct tc_cls *cls, struct rte_mbuf *mbuf, dport = uh->dest; break; + case IPPROTO_SCTP: + if (mbuf_may_pull(mbuf, offset + sizeof(struct sctphdr)) != 0) { + err = TC_ACT_SHOT; + goto done; + } + + sh = rte_pktmbuf_mtod_offset(mbuf, struct sctphdr *, offset); + sport = sh->src_port; + dport = sh->dest_port; + break; + default: /* priv->proto is not assigned */ goto match; } diff --git a/tools/dpip/cls.c b/tools/dpip/cls.c index cab722a68..f38d7e63f 100644 --- a/tools/dpip/cls.c +++ b/tools/dpip/cls.c @@ -49,7 +49,7 @@ static void cls_help(void) " PATTERN := comma seperated of tokens below,\n" " { PROTO | SRANGE | DRANGE | IIF | OIF }\n" " CHILD_QSCH := child qsch handle of the qsch cls attached.\n" - " PROTO := \"{ tcp | udp }\"\n" + " PROTO := \"{ tcp | sctp | udp }\"\n" " SRANGE := \"from=RANGE\"\n" " DRANGE := \"to=RANGE\"\n" " RANGE := ADDR[-ADDR][:PORT[-PORT]]\n" diff --git a/tools/ipvsadm/ipvsadm.c b/tools/ipvsadm/ipvsadm.c index dd234691e..0b6fd76e5 100644 --- a/tools/ipvsadm/ipvsadm.c +++ b/tools/ipvsadm/ipvsadm.c @@ -329,6 +329,7 @@ enum { TAG_SORT, TAG_NO_SORT, TAG_PERSISTENCE_ENGINE, + TAG_SCTP_SERVICE, TAG_SOCKPAIR, TAG_HASH_TARGET, TAG_CPU, @@ -432,6 +433,8 @@ static int parse_dest_check(const char *optarg, struct dest_check_configs *conf) conf->types |= DEST_HC_TCP; } else if (!strcmp(optarg, "udp")) { conf->types |= DEST_HC_UDP; + } else if (!strcmp(optarg, "sctp")) { + conf->types |= DEST_HC_SCTP; } else if (!strcmp(optarg, "ping")) { conf->types |= DEST_HC_PING; } else if (!strcmp(optarg, "default")) { @@ -504,6 +507,8 @@ parse_options(int argc, char **argv, struct ipvs_command_entry *ce, NULL, NULL }, { "udp-service", 'u', POPT_ARG_STRING, &optarg, 'u', NULL, NULL }, + { "sctp-service", '\0', POPT_ARG_STRING, &optarg, + TAG_SCTP_SERVICE, NULL, NULL }, { "icmp-service", 'q', POPT_ARG_STRING, &optarg, 'q', NULL, NULL }, { "icmpv6-service", '1', POPT_ARG_STRING, &optarg, '1', @@ -668,11 +673,14 @@ parse_options(int argc, char **argv, struct ipvs_command_entry *ce, case 'u': case 'q': case '1': + case TAG_SCTP_SERVICE: set_option(options, OPT_SERVICE); if (c == 't') { ce->dpvs_svc.proto = IPPROTO_TCP; } else if (c == 'u') { ce->dpvs_svc.proto = IPPROTO_UDP; + } else if (c == TAG_SCTP_SERVICE) { + ce->dpvs_svc.proto = IPPROTO_SCTP; } else if (c == 'q') { ce->dpvs_svc.proto = IPPROTO_ICMP; } else if (c == '1') { /*a~Z is out. ipvsadm is really not friendly here*/ @@ -1379,7 +1387,7 @@ parse_service(char *buf, dpvs_service_compat_t *dpvs_svc) /* * Get sockpair from the arguments. * sockpair := PROTO:SIP:SPORT:TIP:TPORT - * PROTO := [tcp|udp] + * PROTO := [tcp|udp|sctp] * SIP,TIP := dotted-decimal ip address or square-blacketed ip6 address * SPORT,TPORT := range(0, 65535) */ @@ -1402,6 +1410,8 @@ parse_sockpair(char *buf, ipvs_sockpair_t *sockpair) proto = IPPROTO_TCP; else if (strncmp(pos, "udp", 3) == 0) proto = IPPROTO_UDP; + else if (strncmp(pos, "sctp", 4) == 0) + proto = IPPROTO_SCTP; else return 0; @@ -1474,7 +1484,7 @@ parse_sockpair(char *buf, ipvs_sockpair_t *sockpair) /* * comma separated parameters list, all fields is used to match packets. * - * proto := tcp | udp | icmp |icmpv6 + * proto := tcp | udp | sctp | icmp |icmpv6 * src-range := RANGE * dst-range := RANGE * iif := IFNAME @@ -1510,6 +1520,8 @@ static int parse_match_snat(const char *buf, dpvs_service_compat_t *dpvs_svc) dpvs_svc->proto = IPPROTO_TCP; } else if (strcmp(val, "udp") == 0) { dpvs_svc->proto = IPPROTO_UDP; + } else if (strcmp(val, "sctp") == 0) { + dpvs_svc->proto = IPPROTO_SCTP; } else if (strcmp(val, "icmp") == 0) { dpvs_svc->proto = IPPROTO_ICMP; } else if (strcmp(val, "icmpv6") == 0) { @@ -1685,6 +1697,7 @@ static void usage_exit(const char *program, const int exit_status) "Options:\n" " --tcp-service -t service-address service-address is host[:port]\n" " --udp-service -u service-address service-address is host[:port]\n" + " --sctp-service service-address service-address is host[:port]\n" " --icmp-service -q service-address service-address is host[:port]\n" " --icmpv6-service -1 service-address service-address is host[:port]\n" " --fwmark-service -f fwmark fwmark is an integer greater than zero\n" @@ -1728,7 +1741,7 @@ static void usage_exit(const char *program, const int exit_status) " --cpu cpu_index specifi cpu (lcore) index to show, 0 for master worker\n" " --expire-quiescent expire the quiescent connections timely whose realserver went down\n" " --dest-check CHECK_CONF config health check, inhibit scheduling to failed backends\n" - " CHECK_CONF:=disable|default(passive)|DETAIL(passive)|tcp|udp|ping, DETAIL:=UPDOWN|DOWNONLY\n" + " CHECK_CONF:=disable|default(passive)|DETAIL(passive)|tcp|udp|sctp|ping, DETAIL:=UPDOWN|DOWNONLY\n" " UPDOWN:=down_retry,up_confirm,down_wait,inhibit_min-inhibit_max, for example, the default is 1,1,3s,5-3600s\n" " DOWNONLY:=down_retry,down_wait, for example, --dest-check=1,3s\n" " --laddr -z local-ip local IP\n" @@ -1786,6 +1799,8 @@ static void print_conn_entry(const ipvs_conn_entry_t *conn_entry, snprintf(proto_str, sizeof(proto_str), "%s", "tcp"); else if (conn_entry->proto == IPPROTO_UDP) snprintf(proto_str, sizeof(proto_str), "%s", "udp"); + else if (conn_entry->proto == IPPROTO_SCTP) + snprintf(proto_str, sizeof(proto_str), "%s", "sctp"); else if (conn_entry->proto == IPPROTO_ICMP) snprintf(proto_str, sizeof(proto_str), "%s", "icmp"); else if (conn_entry->proto == IPPROTO_ICMPV6) @@ -2034,6 +2049,8 @@ print_service_entry(dpvs_service_compat_t *se, unsigned int format) proto = "-t"; else if (se->proto == IPPROTO_UDP) proto = "-u"; + else if (se->proto == IPPROTO_SCTP) + proto = "--sctp-service"; else proto = "-q"; @@ -2043,6 +2060,8 @@ print_service_entry(dpvs_service_compat_t *se, unsigned int format) proto = "TCP"; else if (se->proto == IPPROTO_UDP) proto = "UDP"; + else if (se->proto == IPPROTO_SCTP) + proto = "SCTP"; else if (se->proto == IPPROTO_ICMP) proto = "ICMP"; else @@ -2063,6 +2082,8 @@ print_service_entry(dpvs_service_compat_t *se, unsigned int format) proto = "tcp"; else if (se->proto == IPPROTO_UDP) proto = "udp"; + else if (se->proto == IPPROTO_SCTP) + proto = "sctp"; else if (se->proto == IPPROTO_ICMP) proto = "icmp"; else @@ -2203,6 +2224,8 @@ print_service_entry(dpvs_service_compat_t *se, unsigned int format) strcat(buf, "tcp,"); if (se->check_conf.types & DEST_HC_UDP) strcat(buf, "udp,"); + if (se->check_conf.types & DEST_HC_SCTP) + strcat(buf, "sctp,"); if (se->check_conf.types & DEST_HC_PING) strcat(buf, "ping,"); *strrchr(buf, ',') = '\0'; @@ -2409,6 +2432,9 @@ static void print_service_and_blklsts(struct dp_vs_blklst_conf *blklst) case IPPROTO_UDP: snprintf(proto, sizeof(proto), "%s", "UDP"); break; + case IPPROTO_SCTP: + snprintf(proto, sizeof(proto), "%s", "SCTP"); + break; case IPPROTO_ICMP: snprintf(proto, sizeof(proto), "%s", "ICMP"); break; @@ -2509,6 +2535,9 @@ static void print_service_and_whtlsts(struct dp_vs_whtlst_conf *whtlst) case IPPROTO_UDP: snprintf(proto, sizeof(proto), "%s", "UDP"); break; + case IPPROTO_SCTP: + snprintf(proto, sizeof(proto), "%s", "SCTP"); + break; case IPPROTO_ICMP: snprintf(proto, sizeof(proto), "%s", "ICMP"); break; @@ -2706,6 +2735,9 @@ int service_to_port(const char *name, unsigned short proto) else if (proto == IPPROTO_UDP && (service = getservbyname(name, "udp")) != NULL) return ntohs((unsigned short) service->s_port); + else if (proto == IPPROTO_SCTP + && (service = getservbyname(name, "sctp")) != NULL) + return ntohs((unsigned short) service->s_port); else if (proto == IPPROTO_ICMP && (service = getservbyname(name, "icmp")) != NULL) return ntohs((unsigned short) service->s_port); @@ -2727,6 +2759,9 @@ static char * port_to_service(unsigned short port, unsigned short proto) else if (proto == IPPROTO_UDP && (service = getservbyport(htons(port), "udp")) != NULL) return service->s_name; + else if (proto == IPPROTO_SCTP && + (service = getservbyport(htons(port), "sctp")) != NULL) + return service->s_name; else if (proto == IPPROTO_ICMP && (service = getservbyport(htons(port), "icmp")) != NULL) return service->s_name; diff --git a/tools/keepalived/keepalived/check/check_data.c b/tools/keepalived/keepalived/check/check_data.c index ba3ece87e..1fd928e82 100644 --- a/tools/keepalived/keepalived/check/check_data.c +++ b/tools/keepalived/keepalived/check/check_data.c @@ -1128,6 +1128,9 @@ char *dump_vs_match(const virtual_server_t *vs) case IPPROTO_UDP: snprintf(vs_str, sizeof(vs_str) - 1, "%s", "udp"); break; + case IPPROTO_SCTP: + snprintf(vs_str, sizeof(vs_str) - 1, "%s", "sctp"); + break; case IPPROTO_ICMP: snprintf(vs_str, sizeof(vs_str) - 1, "%s", "icmp"); break; From f7b0b50efe92982890a2fc2854a3bf8c1b98eba3 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 19 Apr 2024 10:16:05 +0800 Subject: [PATCH 26/63] ipvs: fix issue #946, a coredump problem when no enough memory on start Signed-off-by: ywc689 --- src/ipvs/ip_vs_conn.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ipvs/ip_vs_conn.c b/src/ipvs/ip_vs_conn.c index e8deb0576..ae3b93cb9 100644 --- a/src/ipvs/ip_vs_conn.c +++ b/src/ipvs/ip_vs_conn.c @@ -1221,9 +1221,8 @@ static int conn_term_lcore(void *arg) if (!rte_lcore_is_enabled(rte_lcore_id())) return EDPVS_DISABLED; - conn_flush(); - if (this_conn_tbl) { + conn_flush(); rte_free(this_conn_tbl); this_conn_tbl = NULL; } From efc101b082f1424797b94a75bc10c509f6aeb487 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 19 Apr 2024 11:04:02 +0800 Subject: [PATCH 27/63] ipvs: fix issue #947, a compiling error caused by string overflow warning with gcc version 8.0+ Signed-off-by: ywc689 --- src/ipvs/ip_vs_proxy_proto.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ipvs/ip_vs_proxy_proto.c b/src/ipvs/ip_vs_proxy_proto.c index 8f08f9228..8e2d35873 100644 --- a/src/ipvs/ip_vs_proxy_proto.c +++ b/src/ipvs/ip_vs_proxy_proto.c @@ -484,8 +484,10 @@ int proxy_proto_insert(struct proxy_info *ppinfo, struct dp_vs_conn *conn, if (unlikely(NULL == inet_ntop(AF_INET, &ppinfo->addr.ip4.dst_addr, tbuf2, sizeof(tbuf2)))) return EDPVS_INVAL; - sprintf(ppv1buf, "PROXY TCP4 %s %s %d %d\r\n", tbuf1, tbuf2, - ntohs(ppinfo->addr.ip4.src_port), ntohs(ppinfo->addr.ip4.dst_port)); + if (unlikely(snprintf(ppv1buf, sizeof(ppv1buf), "PROXY TCP4 %s %s %d %d\r\n", + tbuf1, tbuf2, ntohs(ppinfo->addr.ip4.src_port), + ntohs(ppinfo->addr.ip4.dst_port)) > sizeof(ppv1buf))) + return EDPVS_INVAL; break; case AF_INET6: if (unlikely(NULL == inet_ntop(AF_INET6, ppinfo->addr.ip6.src_addr, @@ -494,8 +496,10 @@ int proxy_proto_insert(struct proxy_info *ppinfo, struct dp_vs_conn *conn, if (unlikely(NULL == inet_ntop(AF_INET6, ppinfo->addr.ip6.dst_addr, tbuf2, sizeof(tbuf2)))) return EDPVS_INVAL; - sprintf(ppv1buf, "PROXY TCP6 %s %s %d %d\r\n", tbuf1, tbuf2, - ntohs(ppinfo->addr.ip6.src_port), ntohs(ppinfo->addr.ip6.dst_port)); + if (unlikely(snprintf(ppv1buf, sizeof(ppv1buf), "PROXY TCP6 %s %s %d %d\r\n", + tbuf1, tbuf2, ntohs(ppinfo->addr.ip6.src_port), + ntohs(ppinfo->addr.ip6.dst_port)) > sizeof(ppv1buf))) + return EDPVS_INVAL; break; default: return EDPVS_NOTSUPP; From 10ae13359d4767ca5d652d62508abb8feee50017 Mon Sep 17 00:00:00 2001 From: Yifeng Sun Date: Thu, 25 Apr 2024 11:44:23 -0700 Subject: [PATCH 28/63] ip_vs_conn: A small improvement Reduce atomic op by one. --- src/ipvs/ip_vs_conn.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/ipvs/ip_vs_conn.c b/src/ipvs/ip_vs_conn.c index e8deb0576..8ade467ac 100644 --- a/src/ipvs/ip_vs_conn.c +++ b/src/ipvs/ip_vs_conn.c @@ -669,7 +669,7 @@ static int dp_vs_conn_expire(void *priv) /* refcnt == 1 means we are the only referer. * no one is using the conn and it's timed out. */ - if (rte_atomic32_read(&conn->refcnt) == 1) { + if (rte_atomic32_sub_return(&conn->refcnt, 1) == 0) { dp_vs_conn_detach_timer(conn, false); /* I was controlled by someone */ @@ -684,8 +684,6 @@ static int dp_vs_conn_expire(void *priv) dp_vs_laddr_unbind(conn); dp_vs_conn_free_packets(conn); - rte_atomic32_dec(&conn->refcnt); - #ifdef CONFIG_DPVS_IPVS_STATS_DEBUG conn_stats_dump("del conn", conn); #endif @@ -696,16 +694,15 @@ static int dp_vs_conn_expire(void *priv) dp_vs_conn_free(conn); return DTIMER_STOP; - } - - dp_vs_conn_hash(conn); + } else { + dp_vs_conn_hash(conn); - /* some one is using it when expire, - * try del it again later */ - dp_vs_conn_refresh_timer(conn, false); + /* some one is using it when expire, + * try del it again later */ + dp_vs_conn_refresh_timer(conn, false); - rte_atomic32_dec(&conn->refcnt); - return DTIMER_OK; + return DTIMER_OK; + } } void dp_vs_conn_expire_now(struct dp_vs_conn *conn) From e19269d63e3dedc337bd40f1cb4fd78ce6d784ed Mon Sep 17 00:00:00 2001 From: ywc689 Date: Tue, 30 Apr 2024 10:45:27 +0800 Subject: [PATCH 29/63] ipvs: improve performance of local addr selection by replacing glibc random with rte_rand Signed-off-by: ywc689 --- src/ipvs/ip_vs_laddr.c | 2 +- src/main.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ipvs/ip_vs_laddr.c b/src/ipvs/ip_vs_laddr.c index 4f15005a9..46db76802 100644 --- a/src/ipvs/ip_vs_laddr.c +++ b/src/ipvs/ip_vs_laddr.c @@ -118,7 +118,7 @@ static inline int __laddr_step(struct dp_vs_service *svc) * */ if (strncmp(svc->scheduler->name, "rr", 2) == 0 || strncmp(svc->scheduler->name, "wrr", 3) == 0) - return (random() % 100) < 5 ? 2 : 1; + return rte_rand_max(100) < 5 ? 2 : 1; return 1; } diff --git a/src/main.c b/src/main.c index 00ca89655..8f8067c0f 100644 --- a/src/main.c +++ b/src/main.c @@ -311,6 +311,7 @@ int main(int argc, char *argv[]) gettimeofday(&tv, NULL); srandom(tv.tv_sec ^ tv.tv_usec ^ getpid()); + rte_srand((uint64_t)(tv.tv_sec ^ tv.tv_usec ^ getpid())); sys_start_time(); if (get_numa_nodes() > DPVS_MAX_SOCKET) { From acce1a7c175c7da87c1d9b0651ebc5f634e56cfc Mon Sep 17 00:00:00 2001 From: ywc689 Date: Mon, 6 May 2024 16:32:12 +0800 Subject: [PATCH 30/63] test: add quic/http3 test programs --- test/quic/.gitignore | 4 + test/quic/Makefile | 29 ++++ test/quic/README.md | 11 ++ test/quic/client/quic-client.go | 85 ++++++++++ test/quic/go.mod | 23 +++ test/quic/go.sum | 214 ++++++++++++++++++++++++ test/quic/http3/certs/cert.pem | 23 +++ test/quic/http3/certs/gen.sh | 6 + test/quic/http3/certs/key.pem | 27 +++ test/quic/http3/certs/req.csr | 19 +++ test/quic/http3/certs/san.conf | 26 +++ test/quic/http3/h3client/h3client.go | 94 +++++++++++ test/quic/http3/h3server/h3server.go | 176 +++++++++++++++++++ test/quic/pkg/cid/cid_generator.go | 77 +++++++++ test/quic/pkg/cid/cid_generator_test.go | 46 +++++ test/quic/pkg/uoa/uoa.go | 120 +++++++++++++ test/quic/server/quic-server.go | 132 +++++++++++++++ 17 files changed, 1112 insertions(+) create mode 100644 test/quic/.gitignore create mode 100644 test/quic/Makefile create mode 100644 test/quic/README.md create mode 100644 test/quic/client/quic-client.go create mode 100644 test/quic/go.mod create mode 100644 test/quic/go.sum create mode 100644 test/quic/http3/certs/cert.pem create mode 100755 test/quic/http3/certs/gen.sh create mode 100644 test/quic/http3/certs/key.pem create mode 100644 test/quic/http3/certs/req.csr create mode 100644 test/quic/http3/certs/san.conf create mode 100644 test/quic/http3/h3client/h3client.go create mode 100644 test/quic/http3/h3server/h3server.go create mode 100644 test/quic/pkg/cid/cid_generator.go create mode 100644 test/quic/pkg/cid/cid_generator_test.go create mode 100644 test/quic/pkg/uoa/uoa.go create mode 100644 test/quic/server/quic-server.go diff --git a/test/quic/.gitignore b/test/quic/.gitignore new file mode 100644 index 000000000..a52aaf740 --- /dev/null +++ b/test/quic/.gitignore @@ -0,0 +1,4 @@ +client/quic-client +server/quic-server +http3/h3client/h3client +http3/h3server/h3server diff --git a/test/quic/Makefile b/test/quic/Makefile new file mode 100644 index 000000000..d5d229162 --- /dev/null +++ b/test/quic/Makefile @@ -0,0 +1,29 @@ +TARGETS := quic-client quic-server h3client h3server + +GO ?= go +LD_FLAGS = -ldflags="-s -w" +GO_BUILD = CGO_ENABLED=0 $(GO) build $(LD_FLAGS) +RM ?= rm + + +.PHONY: all $(TARGET) clean version_notes + +all: version_notes $(TARGETS) + +quic-client: client/quic-client.go + $(GO_BUILD) -o $@ $< + +quic-server: server/quic-server.go + $(GO_BUILD) -o $@ $< + +h3client: http3/h3client/h3client.go + $(GO_BUILD) -o $@ $< + +h3server: http3/h3server/h3server.go + $(GO_BUILD) -o $@ $< + +clean: + @-$(RM) $(TARGETS) + +version_notes: + $(info "Notes: $(shell go version), v1.21+ is required") diff --git a/test/quic/README.md b/test/quic/README.md new file mode 100644 index 000000000..a113c0abd --- /dev/null +++ b/test/quic/README.md @@ -0,0 +1,11 @@ +The test programs in this directory are built with QUIC library [quic-go](https://github.com/quic-go/quic-go). + +The version requirements are shown as below. +* Quic-go: v0.42.0 +* Golang: v1.21.8 + +Quic-go may not well support ECN in such distros as Centos 7 (refer to [issue #4396](https://github.com/quic-go/quic-go/issues/4396) for details), in which case the ECN should be disabled using environment varible `QUIC_GO_DISABLE_ECN`. + +```sh +export QUIC_GO_DISABLE_ECN=true +``` diff --git a/test/quic/client/quic-client.go b/test/quic/client/quic-client.go new file mode 100644 index 000000000..45683f11f --- /dev/null +++ b/test/quic/client/quic-client.go @@ -0,0 +1,85 @@ +package main + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "log" + "net" + "os" + + "github.com/quic-go/quic-go" +) + +var servAddr = ":4242" + +var keyLogFile = "quic-go-client-sshkey.log" + +func main() { + if len(os.Args) > 1 { + servAddr = os.Args[1] + } + fmt.Printf("target server: %s\n", servAddr) + + keyLog, err := os.OpenFile(keyLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Fatal(err) + } + defer keyLog.Close() + + ctx := context.Background() + tlsConf := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"quic-echo-example"}, + KeyLogWriter: keyLog, + } + + /* + conn, err := quic.DialAddr(ctx, servAddr, tlsConf, nil) + if err != nil { + log.Fatal("Cannot dial QUIC server:", err) + } + defer conn.CloseWithError(0, "") + */ + + serverAddr, err := net.ResolveUDPAddr("udp", servAddr) + if err != nil { + log.Fatal("ServerAddr resolution fail:", err) + } + + listenAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0} + listener, err := net.ListenUDP("udp", listenAddr) + if err != nil { + log.Fatal("Listener creation falil:", err) + } + defer listener.Close() + + conn, err := quic.Dial(ctx, listener, serverAddr, tlsConf, nil) + if err != nil { + log.Fatal("Cannot dial QUIC server:", err) + } + + stream, err := conn.OpenStreamSync(ctx) + if err != nil { + log.Fatal("Cannot open QUIC stream:", err) + } + defer stream.Close() + + message := []byte("Hello, QUIC Server!") + _, err = stream.Write(message) + if err != nil { + log.Fatal("Cannot write to QUIC stream:", err) + } + + buffer := make([]byte, len(message)) + _, err = io.ReadFull(stream, buffer) + if err != nil { + log.Fatal("Cannot read from QUIC stream:", err) + } + + fmt.Printf("Server says: %s\n", buffer) + + // TODO: Support connection migration. + // Awaiting quic-go support the feature https://github.com/quic-go/quic-go/issues/3990. +} diff --git a/test/quic/go.mod b/test/quic/go.mod new file mode 100644 index 000000000..4b60204d2 --- /dev/null +++ b/test/quic/go.mod @@ -0,0 +1,23 @@ +module quic-test + +go 1.21.8 + +require ( + github.com/quic-go/quic-go v0.43.1 + golang.org/x/sys v0.20.0 +) + +require ( + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + go.uber.org/mock v0.4.0 // indirect + golang.org/x/crypto v0.4.0 // indirect + golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect + golang.org/x/mod v0.11.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.9.1 // indirect +) diff --git a/test/quic/go.sum b/test/quic/go.sum new file mode 100644 index 000000000..0926d4fc4 --- /dev/null +++ b/test/quic/go.sum @@ -0,0 +1,214 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/quic-go v0.43.1 h1:fLiMNfQVe9q2JvSsiXo4fXOEguXHGGl9+6gLp4RPeZQ= +github.com/quic-go/quic-go v0.43.1/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/test/quic/http3/certs/cert.pem b/test/quic/http3/certs/cert.pem new file mode 100644 index 000000000..822bc32c4 --- /dev/null +++ b/test/quic/http3/certs/cert.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDzDCCArSgAwIBAgIJALD9Trd6ieu1MA0GCSqGSIb3DQEBCwUAMIGPMQswCQYD +VQQGEwJDTjERMA8GA1UECAwIU2hhbmdIYWkxEjAQBgNVBAcMCUNoYW5nTmluZzEO +MAwGA1UECgwFSVFJWUkxEDAOBgNVBAsMB0lJRy9RTEIxITAfBgkqhkiG9w0BCQEW +Enl1d2VuY2hhb0BxaXlpLmNvbTEUMBIGA1UEAwwLKi5pcWl5aS5jb20wHhcNMjQw +NTA2MDY0MjIxWhcNMzQwNTA0MDY0MjIxWjCBjzELMAkGA1UEBhMCQ04xETAPBgNV +BAgMCFNoYW5nSGFpMRIwEAYDVQQHDAlDaGFuZ05pbmcxDjAMBgNVBAoMBUlRSVlJ +MRAwDgYDVQQLDAdJSUcvUUxCMSEwHwYJKoZIhvcNAQkBFhJ5dXdlbmNoYW9AcWl5 +aS5jb20xFDASBgNVBAMMCyouaXFpeWkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA0+nv99AO31eiPQ6jZpdT2nn3AMlw3rq/JfYP2cFerbJqBE9p +98yNkgLYVMOSSpSHtZT3MckiwEUTX498CZ1zfFbsFoW9o+waJVV7swtWiqvquf7v +UdX8dknJQxxsnPhvEyesI7UIPhBQZ16J+rQW9TM30BSrl5Mb2BPS4PgvTeMsKDU6 +2lhbyEnEYepr32nRnwkz1QBflcvXzmPWbly8GUnQVYQqKdNtGT3yfyiKqJUZ5b2L +3WdvTwOxmjIQi1perwfFl1OFlXbCKv6VtkbaHZ27rBETECaLJ7vrRf6+USoo5RnB +SngSXrPq6BN9/u/kLSkLb+qH451lunrCEPLcSQIDAQABoykwJzAlBgNVHREEHjAc +ggsqLmlxaXlpLmNvbYINKi5xaXlpLmRvbWFpbjANBgkqhkiG9w0BAQsFAAOCAQEA +jQguPoHjs2r2JXob4B66gn5fTiLMsfLz/c66WDvS+uECCfcmubGY8IcB2eYUF1ut +gLDjfycxamjBK5iSBKLxkwvtG5hUbkY8kuPcx0I4G6peHWaGdZGUOVOjACZkoWKT +ztgD3sdr7M0FpS8dTomtT3jrEFUbmNGjlt+BvPVP5yx9PYR3vfLvRwgKzQzVXgZs +pgTL6WWhFEeAEC2r0y0u+j7Kj0p1RgFyVT5l56LVLXhqdD7dsddKKGRU435kfFem +F5K/Q7XPJUa3aixIni//LKc7XQ2Xlu4iSO7MWNKR97gaoVakVp1PWh/c4LVBLx6M +nMtHmVzrQii86eQI79OlOA== +-----END CERTIFICATE----- diff --git a/test/quic/http3/certs/gen.sh b/test/quic/http3/certs/gen.sh new file mode 100755 index 000000000..1122e8b1c --- /dev/null +++ b/test/quic/http3/certs/gen.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# + +openssl genrsa -out key.pem 2048 +openssl req -new -key key.pem -out req.csr -config san.conf +openssl x509 -req -days 3650 -in req.csr -signkey key.pem -out cert.pem -extensions req_ext -extfile san.conf diff --git a/test/quic/http3/certs/key.pem b/test/quic/http3/certs/key.pem new file mode 100644 index 000000000..057d77dff --- /dev/null +++ b/test/quic/http3/certs/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA0+nv99AO31eiPQ6jZpdT2nn3AMlw3rq/JfYP2cFerbJqBE9p +98yNkgLYVMOSSpSHtZT3MckiwEUTX498CZ1zfFbsFoW9o+waJVV7swtWiqvquf7v +UdX8dknJQxxsnPhvEyesI7UIPhBQZ16J+rQW9TM30BSrl5Mb2BPS4PgvTeMsKDU6 +2lhbyEnEYepr32nRnwkz1QBflcvXzmPWbly8GUnQVYQqKdNtGT3yfyiKqJUZ5b2L +3WdvTwOxmjIQi1perwfFl1OFlXbCKv6VtkbaHZ27rBETECaLJ7vrRf6+USoo5RnB +SngSXrPq6BN9/u/kLSkLb+qH451lunrCEPLcSQIDAQABAoIBACsyQX8jQxTVuTV2 ++WndKPOc7vOTHFXafUJQsRRzLUh82M1+HpyrbqQ3vj8xCm33bt5dujHEzTeiHPva +tK8FEFhlI4THyEtUwlOf5DIv+nkT6Cn3xHLLCsZV7henAKDSp1mhOZ6htUdpbepU +RA39jbx3r0XUINIp44AdMlw3WvUmHZCRU2sKLEanrmx8FOvQ6eDyAbo3qNoo/AGI +2iTrM1xlHpSQjHg6DVZ+2XDp/VHMZTTDPw9DkgugRdmFKR06apxh6RIVNbJOLfAR +Iyx0lNHo5weEWW1nQEC+bXfgejHhcx5HrTRKqbqeBpUxx2NEZKW7TTik11jUcHPR +tlTXoQECgYEA68HVOWfK47m58S226VBv59Egtu/y8TRv3iyS2ONhFkRD8ZORd2Gi +NrwSPQeF1JIpRJeCoNSXsbdi69qmZhKUW4Vx16OdhZG0vsPsK2LRaoy+A+maX2Ye +ZHAnp7nIDG3KBfzy3PCXC4SVWlxxcw3BTECVPtGxCeVEgZkGB9AGUekCgYEA5hwB +TUTWVxmBOKGOLptb17eQ8BlcP2o8JFsbcft7pum6ouECS+ZJmJ80BuBzFiMc18Fk +S28rM0ACF8BK8J7XJbK+xs1IlTpbHzQdo+tBImfjqqTeWyx2TrEDKB6VIRqhLQyf +jYYNcktSEjy2XRfvG5KC/oAs9axo7N02hW3wW2ECgYEAjU9HcPsnf0vpiggupLZT +/Q06oKw+YBlgHDl3Y40WunP8jaY4AOiChHBCNlZ1/y4EklqGL8R9kEYtgtUx++iT +CDB6RhiJ6G+neNiSjIbUoxrtIgc5QolBGk6nVj9jCyAbgW9WWtvSjVLQ+rKCRcYu +4HetfVPO2/GSGGQSW0hzIVECgYEAy8zp6kGZhLL2G+4aO3UltrzCBaSwawnwElMO +z7joH0DLKA8ZNZfUfvQh5CVOSMD4fq6t4ZGoNU/vipGozcwgySayiOiv7Fsu8Uf7 +KH7nxU01+qDivuV2MuPb4+CSPCuVrIyNk46ywhOrsLNM4M6d21G76yQircPxejfC +XhKs2oECgYEAvqmwICb/UKLwmQ39IY8o0/XA21oZFOTH28qS9ah7uCwNEUl5FmHW +oN6sgiaZyl8uxA/SoB2RBaJy4BxhyL+KEehrhhIFMVBAlNkgf36YDFS3XzoamKIB +Aoev43M9/VqXUM5xUMCOp7Dxo6NPv9uVio+VyVZD7i+3ZNFbhEgU4Rk= +-----END RSA PRIVATE KEY----- diff --git a/test/quic/http3/certs/req.csr b/test/quic/http3/certs/req.csr new file mode 100644 index 000000000..67bdceb87 --- /dev/null +++ b/test/quic/http3/certs/req.csr @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIDDTCCAfUCAQAwgY8xCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhTaGFuZ0hhaTES +MBAGA1UEBwwJQ2hhbmdOaW5nMQ4wDAYDVQQKDAVJUUlZSTEQMA4GA1UECwwHSUlH +L1FMQjEhMB8GCSqGSIb3DQEJARYSeXV3ZW5jaGFvQHFpeWkuY29tMRQwEgYDVQQD +DAsqLmlxaXlpLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANPp +7/fQDt9Xoj0Oo2aXU9p59wDJcN66vyX2D9nBXq2yagRPaffMjZIC2FTDkkqUh7WU +9zHJIsBFE1+PfAmdc3xW7BaFvaPsGiVVe7MLVoqr6rn+71HV/HZJyUMcbJz4bxMn +rCO1CD4QUGdeifq0FvUzN9AUq5eTG9gT0uD4L03jLCg1OtpYW8hJxGHqa99p0Z8J +M9UAX5XL185j1m5cvBlJ0FWEKinTbRk98n8oiqiVGeW9i91nb08DsZoyEItaXq8H +xZdThZV2wir+lbZG2h2du6wRExAmiye760X+vlEqKOUZwUp4El6z6ugTff7v5C0p +C2/qh+OdZbp6whDy3EkCAwEAAaA4MDYGCSqGSIb3DQEJDjEpMCcwJQYDVR0RBB4w +HIILKi5pcWl5aS5jb22CDSoucWl5aS5kb21haW4wDQYJKoZIhvcNAQELBQADggEB +AH78c1QWE2+U4WcnHVBrXM0sNNuSx2ZszPXeb15fg+DQnYTtSKihSjrnhZ1jtyRT +jaMILXhz0CAdWB0mA9AqBCmq4CxDV0iJR7v8ndtLaLFCRAveHY9MPfrjY9jByLLO +Rv16bHyHeKmrrrFSxstmuJmPP7OJgqSuJKNdwEvIXkvlh3I21HZoS/jksoMMc9E5 +2m97k2jHGV5Jqs7W6SsDWltpD5DOkcmvhngk6jPjF5B6KhjNo4Askvv2nKnC/Re+ +t0pJTEBmLeDTmFGXQ+38PVeYz9bfOUrugZUhANZ0QQ5RyY8Be6pCOwoUB2cPJ93T +GgrEFtKcNBp1BZliLpfg/JI= +-----END CERTIFICATE REQUEST----- diff --git a/test/quic/http3/certs/san.conf b/test/quic/http3/certs/san.conf new file mode 100644 index 000000000..0ad6f5779 --- /dev/null +++ b/test/quic/http3/certs/san.conf @@ -0,0 +1,26 @@ +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = req_ext +x509_extensions = x509_ext + +[dn] +C = CN +ST = ShangHai +L = ChangNing +O = IQIYI +OU = IIG/QLB +emailAddress=yuwenchao@qiyi.com +CN = *.iqiyi.com + +[req_ext] +subjectAltName = @alt_names + +[x509_ext] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = *.iqiyi.com +DNS.2 = *.qiyi.domain diff --git a/test/quic/http3/h3client/h3client.go b/test/quic/http3/h3client/h3client.go new file mode 100644 index 000000000..a040d1204 --- /dev/null +++ b/test/quic/http3/h3client/h3client.go @@ -0,0 +1,94 @@ +package main + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "flag" + "io" + "log" + "net/http" + "os" + "sync" + + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" + "github.com/quic-go/quic-go/qlog" +) + +func main() { + quiet := flag.Bool("q", false, "don't print the data") + keyLogFile := flag.String("keylog", "", "key log file") + insecure := flag.Bool("insecure", false, "skip certificate verification") + cert := flag.String("cert", "../certs/cert.pem", "TLS certificate") + flag.Parse() + urls := flag.Args() + + var keyLog io.Writer + if len(*keyLogFile) > 0 { + f, err := os.Create(*keyLogFile) + if err != nil { + log.Fatal(err) + } + defer f.Close() + keyLog = f + } + + pool, err := x509.SystemCertPool() + if err != nil { + log.Fatal(err) + } + + if *cert != "" { + if _, err = os.Stat(*cert); err == nil { + caCertRaw, err := os.ReadFile(*cert) + if err != nil { + panic(err) + } + if ok := pool.AppendCertsFromPEM(caCertRaw); !ok { + panic("Could not add root certificate to pool.") + } + } + } + + roundTripper := &http3.RoundTripper{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + InsecureSkipVerify: *insecure, + KeyLogWriter: keyLog, + }, + QUICConfig: &quic.Config{ + Tracer: qlog.DefaultTracer, + }, + } + defer roundTripper.Close() + hclient := &http.Client{ + Transport: roundTripper, + } + + var wg sync.WaitGroup + wg.Add(len(urls)) + for _, addr := range urls { + log.Printf("GET %s", addr) + go func(addr string) { + rsp, err := hclient.Get(addr) + if err != nil { + log.Fatal(err) + } + log.Printf("Got response for %s: %#v", addr, rsp) + + body := &bytes.Buffer{} + _, err = io.Copy(body, rsp.Body) + if err != nil { + log.Fatal(err) + } + if *quiet { + log.Printf("Response Body: %d bytes", body.Len()) + } else { + log.Printf("Response Body (%d bytes):\n%s", body.Len(), body.Bytes()) + } + wg.Done() + }(addr) + } + wg.Wait() +} diff --git a/test/quic/http3/h3server/h3server.go b/test/quic/http3/h3server/h3server.go new file mode 100644 index 000000000..c739bdb90 --- /dev/null +++ b/test/quic/http3/h3server/h3server.go @@ -0,0 +1,176 @@ +package main + +import ( + "crypto/md5" + "errors" + "flag" + "fmt" + "io" + "log" + "mime/multipart" + "net/http" + "strconv" + "strings" + "sync" + + _ "net/http/pprof" + + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" + "github.com/quic-go/quic-go/qlog" +) + +type binds []string + +func (b binds) String() string { + return strings.Join(b, ",") +} + +func (b *binds) Set(v string) error { + *b = strings.Split(v, ",") + return nil +} + +// Size is needed by the /demo/upload handler to determine the size of the uploaded file +type Size interface { + Size() int64 +} + +// See https://en.wikipedia.org/wiki/Lehmer_random_number_generator +func generatePRData(l int) []byte { + res := make([]byte, l) + seed := uint64(1) + for i := 0; i < l; i++ { + seed = seed * 48271 % 2147483647 + res[i] = byte(seed) + } + return res +} + +func setupHandler(www string) http.Handler { + mux := http.NewServeMux() + + if len(www) > 0 { + mux.Handle("/", http.FileServer(http.Dir(www))) + } else { + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%#v\n", r) + const maxSize = 1 << 30 // 1 GB + num, err := strconv.ParseInt(strings.ReplaceAll(r.RequestURI, "/", ""), 10, 64) + if err != nil || num <= 0 || num > maxSize { + w.WriteHeader(400) + return + } + w.Write(generatePRData(int(num))) + }) + } + + mux.HandleFunc("/demo/tile", func(w http.ResponseWriter, r *http.Request) { + // Small 40x40 png + w.Write([]byte{ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x28, + 0x01, 0x03, 0x00, 0x00, 0x00, 0xb6, 0x30, 0x2a, 0x2e, 0x00, 0x00, 0x00, + 0x03, 0x50, 0x4c, 0x54, 0x45, 0x5a, 0xc3, 0x5a, 0xad, 0x38, 0xaa, 0xdb, + 0x00, 0x00, 0x00, 0x0b, 0x49, 0x44, 0x41, 0x54, 0x78, 0x01, 0x63, 0x18, + 0x61, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x01, 0xe2, 0xb8, 0x75, 0x22, 0x00, + 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, + }) + }) + + mux.HandleFunc("/demo/tiles", func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "") + for i := 0; i < 200; i++ { + fmt.Fprintf(w, ``, i) + } + io.WriteString(w, "") + }) + + mux.HandleFunc("/demo/echo", func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + fmt.Printf("error reading body while handling /echo: %s\n", err.Error()) + } + w.Write(body) + }) + + // accept file uploads and return the MD5 of the uploaded file + // maximum accepted file size is 1 GB + mux.HandleFunc("/demo/upload", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + err := r.ParseMultipartForm(1 << 30) // 1 GB + if err == nil { + var file multipart.File + file, _, err = r.FormFile("uploadfile") + if err == nil { + var size int64 + if sizeInterface, ok := file.(Size); ok { + size = sizeInterface.Size() + b := make([]byte, size) + file.Read(b) + md5 := md5.Sum(b) + fmt.Fprintf(w, "%x", md5) + return + } + err = errors.New("couldn't get uploaded file size") + } + } + log.Printf("Error receiving upload: %#v", err) + } + io.WriteString(w, `
+
+ +
`) + }) + + return mux +} + +func main() { + // defer profile.Start().Stop() + go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() + // runtime.SetBlockProfileRate(1) + + bs := binds{} + flag.Var(&bs, "bind", "bind to") + www := flag.String("www", "", "www data") + tcp := flag.Bool("tcp", false, "also listen on TCP") + key := flag.String("key", "../certs/key.pem", "TLS key (requires -cert option)") + cert := flag.String("cert", "../certs/cert.pem", "TLS certificate (requires -key option)") + flag.Parse() + + if len(bs) == 0 { + bs = binds{"localhost:6121"} + } + + handler := setupHandler(*www) + + var wg sync.WaitGroup + wg.Add(len(bs)) + for _, b := range bs { + fmt.Println("listening on", b) + bCap := b + go func() { + var err error + if *tcp { + err = http3.ListenAndServe(bCap, *cert, *key, handler) + } else { + server := http3.Server{ + Handler: handler, + Addr: bCap, + QUICConfig: &quic.Config{ + Tracer: qlog.DefaultTracer, + }, + } + err = server.ListenAndServeTLS(*cert, *key) + } + if err != nil { + fmt.Println(err) + } + wg.Done() + }() + } + wg.Wait() +} diff --git a/test/quic/pkg/cid/cid_generator.go b/test/quic/pkg/cid/cid_generator.go new file mode 100644 index 000000000..1a99db813 --- /dev/null +++ b/test/quic/pkg/cid/cid_generator.go @@ -0,0 +1,77 @@ +package cid + +import ( + "crypto/rand" + "fmt" + "io" + "net" +) + +const ( + QUIC_CID_BUF_LEN = 20 + DPVS_QUIC_DCID_BYTES_MIN = 7 +) + +func QuicCIDGenerator( + cidLen uint8, // the total length of CID to be generated, 7~20 bytes + l3len uint8, // the length of server IP to encode into CID, 1~8 bytes + l4len uint8, // the length of server Port to encode into CID, 0 or 2 bytes + svrIP net.IP, // the server IP + svrPort uint16, // the server Port +) (error, []byte) { + rdbuf := make([]byte, QUIC_CID_BUF_LEN) + var i uint8 + var l3addr []byte + var l4addr uint16 + + if cidLen < DPVS_QUIC_DCID_BYTES_MIN || l3len > 8 || l3len < 1 || + (l4len != 0 && l4len != 2) || + cidLen < l3len+l4len+5 { + return fmt.Errorf("invalid params"), nil + } + + entropy := cidLen - l3len - l4len + 1 + l4flag := 0 + if l4len > 0 { + l4flag = 1 + } + + ipbytes := svrIP.To4() + if ipbytes != nil { + l3addr = ipbytes[4-l3len:] + } else { + ipbytes = svrIP.To16() + if ipbytes == nil { + return fmt.Errorf("invalid IP %v", svrIP), nil + } + l3addr = ipbytes[16-l3len:] + } + l4addr = svrPort + + if _, err := io.ReadFull(rand.Reader, rdbuf[:entropy]); err != nil { + return err, nil + } + + cid := make([]byte, cidLen, cidLen) + cid[0] = rdbuf[0] + cid[1] = uint8(((l3len-1)&0x7)<<5) | uint8((l4flag&0x1)<<4) | ((uint8(l3addr[0]) >> 4) & 0xf) + for i = 0; i < l3len; i++ { + if i == l3len-1 { + cid[2+i] = ((l3addr[0] & 0xf) << 4) + } else { + cid[2+i] = ((l3addr[0] & 0xf) << 4) | ((l3addr[1] >> 4) & 0xf) + } + l3addr = l3addr[1:] + } + if l4len > 0 { + cid[l3len+1] &= 0xf0 + cid[l3len+1] |= byte((l4addr >> 12) & 0xf) + l4addr <<= 4 + cid[l3len+2] = byte((l4addr >> 8) & 0xff) + cid[l3len+3] = byte(l4addr & 0xff) + } + cid[l3len+l4len+1] |= (rdbuf[1] & 0xf) + copy(cid[l3len+l4len+2:], rdbuf[2:entropy-1]) + + return nil, cid +} diff --git a/test/quic/pkg/cid/cid_generator_test.go b/test/quic/pkg/cid/cid_generator_test.go new file mode 100644 index 000000000..d527a868f --- /dev/null +++ b/test/quic/pkg/cid/cid_generator_test.go @@ -0,0 +1,46 @@ +package cid + +import ( + "net" + "reflect" + "testing" +) + +func TestQuicCIDGenerator_IPv4(t *testing.T) { + err, cid := QuicCIDGenerator(10, 3, 2, net.ParseIP("192.168.111.222"), 8029) + if err != nil { + t.Errorf("QuicCIDGenerator error return: %v", err) + } + if len(cid) != 10 { + t.Errorf("invalid CID length") + } + result := make([]byte, 6) + copy(result, cid[1:7]) + result[len(result)-1] &= 0xf0 + expected := []byte{0x5a, 0x86, 0xfd, 0xe1, 0xf5, 0xd0} + + if !reflect.DeepEqual(result, expected) { + t.Errorf("mismatched CID:\nresult: %x\nexpect: %x\n", result, expected) + } else { + t.Logf("%x\n", cid) + } +} + +func TestQuicCIDGenerator_IPv6(t *testing.T) { + err, cid := QuicCIDGenerator(16, 6, 2, net.ParseIP("2001::123:4567:89ab:cdef"), 51321) + if err != nil { + t.Errorf("QuicCIDGenerator error return: %v", err) + } + if len(cid) != 16 { + t.Errorf("invalid CID length") + } + result := make([]byte, 9) + copy(result, cid[1:10]) + result[len(result)-1] &= 0xf0 + expected := []byte{0xb4, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xfc, 0x87, 0x90} + if !reflect.DeepEqual(result, expected) { + t.Errorf("mismatched CID:\nresult: %x\nexpect: %x\n", result, expected) + } else { + t.Logf("%x\n", cid) + } +} diff --git a/test/quic/pkg/uoa/uoa.go b/test/quic/pkg/uoa/uoa.go new file mode 100644 index 000000000..5485aeae2 --- /dev/null +++ b/test/quic/pkg/uoa/uoa.go @@ -0,0 +1,120 @@ +package uoa + +import ( + "encoding/binary" + "fmt" + "net" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + IPOPT_CONTROL = 0 + IPOPT_UOA = 31 | IPOPT_CONTROL + UOA_SO_GET_LOOKUP = 2048 + + AF_INET = 2 + AF_INET6 = 10 +) + +type uoaParamMap struct { + // input + af uint16 + saddr [16]byte + daddr [16]byte + sport uint16 + dport uint16 + + // output + realAf uint16 + realSaddr [16]byte + realSport uint16 +} + +func AddrToIPnPort(addr net.Addr) (net.IP, uint16, error) { + switch t := addr.(type) { + case *net.TCPAddr: + return t.IP, uint16(t.Port), nil + case *net.UDPAddr: + return t.IP, uint16(t.Port), nil + default: + return nil, 0, fmt.Errorf("unsupported address type %T for %s", t, addr) + } +} + +func IPnPortToAddr(af uint16, l4Proto string, addr [16]byte, port uint16) (net.Addr, error) { + // fmt.Println("uoa address", af, l4Proto, addr, port) + switch l4Proto { + case "tcp": + res := &net.TCPAddr{} + if af == AF_INET { + res.IP = net.IPv4(addr[0], addr[1], addr[2], addr[3]) + } else { + res.IP = make(net.IP, net.IPv6len) + copy(res.IP, addr[:]) + } + res.Port = int(port) + return res, nil + case "udp": + res := &net.UDPAddr{} + if af == AF_INET { + res.IP = net.IPv4(addr[0], addr[1], addr[2], addr[3]) + } else { + res.IP = make(net.IP, net.IPv6len) + copy(res.IP, addr[:]) + } + res.Port = int(port) + return res, nil + default: + return nil, fmt.Errorf("unsupported network type %q", l4Proto) + } +} + +func htons(le uint16) uint16 { + bytes := make([]byte, 2) + binary.LittleEndian.PutUint16(bytes, le) + return binary.BigEndian.Uint16(bytes) +} + +func ntohs(be uint16) uint16 { + bytes := make([]byte, 2) + binary.BigEndian.PutUint16(bytes, be) + return binary.LittleEndian.Uint16(bytes) +} + +func GetUoaAddr(fd uintptr, saddr, daddr net.Addr) (net.Addr, error) { + sip, sport, err := AddrToIPnPort(saddr) + if err != nil { + return nil, err + } + _, dport, err := AddrToIPnPort(daddr) // server ip doesn't matter + if err != nil { + return nil, err + } + uoaParam := uoaParamMap{} + if sip.To4() != nil { + uoaParam.af = AF_INET + copy(uoaParam.saddr[:], sip.To4()) + //copy(uoaParam.daddr[:], dip.To4()) + } else { + uoaParam.af = AF_INET6 + copy(uoaParam.saddr[:], sip.To16()) + //copy(uoaParam.daddr[:], dip.To16()) + } + uoaParam.sport = htons(sport) + uoaParam.dport = htons(dport) + paramLen := uint32(unsafe.Sizeof(uoaParam)) + //fmt.Println(fd, uoaParam, paramLen) + _, _, errno := unix.Syscall6(unix.SYS_GETSOCKOPT, fd, unix.IPPROTO_IP, UOA_SO_GET_LOOKUP, uintptr(unsafe.Pointer(&uoaParam)), uintptr(unsafe.Pointer(¶mLen)), 0) + if errno != 0 { + return nil, fmt.Errorf("syscall failed with errno %d", errno) + } + + res, err := IPnPortToAddr(uoaParam.realAf, "udp", uoaParam.realSaddr, ntohs(uoaParam.realSport)) + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/test/quic/server/quic-server.go b/test/quic/server/quic-server.go new file mode 100644 index 000000000..27c34dbe4 --- /dev/null +++ b/test/quic/server/quic-server.go @@ -0,0 +1,132 @@ +package main + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "net" + "os" + + quic "github.com/quic-go/quic-go" + + "quic-test/pkg/uoa" +) + +var servAddr = ":4242" + +var keyLogFile = "quic-go-server-sshkey.log" + +func main() { + if len(os.Args) > 1 { + servAddr = os.Args[1] + } + fmt.Printf("Quic Server listens on %s\n", servAddr) + + keyLog, err := os.Create(keyLogFile) + if err != nil { + log.Fatal(err) + } + defer keyLog.Close() + + tlsConf := generateTLSConfig() + tlsConf.KeyLogWriter = keyLog + + /* + listener, err := quic.ListenAddr(servAddr, tlsConf, nil) + if err != nil { + panic(err) + } + */ + udpAddr, err := net.ResolveUDPAddr("udp", servAddr) + if err != nil { + panic(err) + } + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + panic(err) + } + listener, err := (&quic.Transport{ + Conn: udpConn, + }).Listen(tlsConf, nil) + if err != nil { + panic(err) + } + defer listener.Close() + + ctx := context.Background() + for { + sess, err := listener.Accept(ctx) + if err != nil { + panic(err) + } + go handleSession(ctx, udpConn, sess) + } +} + +func handleSession(ctx context.Context, udpConn *net.UDPConn, sess quic.Connection) { + file, err := udpConn.File() + if err != nil { + panic(err) + } + uoaAddr, err := uoa.GetUoaAddr(file.Fd(), sess.RemoteAddr(), sess.LocalAddr()) + if err != nil { + fmt.Printf("New connection from %v, uoaAddr failed for %v\n", sess.RemoteAddr(), err) + } else { + fmt.Printf("New connection from %v, uoaAddr %v\n", sess.RemoteAddr(), uoaAddr) + } + stream, err := sess.AcceptStream(ctx) + if err != nil { + panic(err) + } + defer stream.Close() + + fmt.Printf("accepted new conn stream: %v\n", stream.StreamID()) + + // the server simply echo the received data back to client + buffer := make([]byte, 32) + _, err = stream.Read(buffer) + if err != nil { + panic(err) + } + fmt.Printf("got data: %s\n", buffer) + + buffer = []byte("Hello, QUIC Client!") + _, err = stream.Write(buffer) + if err != nil { + panic(err) + } + fmt.Printf("sent data: %s\n", buffer) +} + +// generateTLSConfig creates TLS configs required for TLS handshake. +// In fact, you should use an authorized certificate released from CA. +func generateTLSConfig() *tls.Config { + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + template := x509.Certificate{SerialNumber: big.NewInt(1)} + template.Subject = pkix.Name{Organization: []string{"quic-go"}} + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + panic(err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + panic(err) + } + return &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + NextProtos: []string{"quic-echo-example"}, + } +} From e2f953379e6dc5946a977ec3c7034891410a13fb Mon Sep 17 00:00:00 2001 From: ywc689 Date: Mon, 13 May 2024 17:35:28 +0800 Subject: [PATCH 31/63] test: quic server supports dpvs-compatible cid and uoa --- test/quic/client/quic-client.go | 54 +++++++++++----- test/quic/http3/h3client/h3client.go | 2 +- test/quic/http3/h3server/h3server.go | 4 +- test/quic/pkg/cid/cid_generator.go | 68 +++++++++++++++++--- test/quic/pkg/cid/cid_generator_test.go | 5 +- test/quic/pkg/cid/server_addr.go | 25 ++++++++ test/quic/server/quic-server.go | 83 ++++++++++++++++++------- 7 files changed, 189 insertions(+), 52 deletions(-) create mode 100644 test/quic/pkg/cid/server_addr.go diff --git a/test/quic/client/quic-client.go b/test/quic/client/quic-client.go index 45683f11f..764910597 100644 --- a/test/quic/client/quic-client.go +++ b/test/quic/client/quic-client.go @@ -3,47 +3,61 @@ package main import ( "context" "crypto/tls" + "flag" "fmt" "io" "log" "net" "os" + "runtime/trace" "github.com/quic-go/quic-go" ) -var servAddr = ":4242" - -var keyLogFile = "quic-go-client-sshkey.log" - func main() { - if len(os.Args) > 1 { - servAddr = os.Args[1] - } - fmt.Printf("target server: %s\n", servAddr) + servAddr := flag.String("server", ":4242", "quic server address") + keyLogFile := flag.String("keylog", "", "key log file") + traceFile := flag.String("trace", "", "trace file name") + flag.Parse() - keyLog, err := os.OpenFile(keyLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - log.Fatal(err) + if *traceFile != "" { + tracef, err := os.Create(*traceFile) + if err != nil { + log.Fatalf("failed to create trace output file: %v", err) + } + defer tracef.Close() + err = trace.Start(tracef) + if err != nil { + log.Fatalf("failed to start trace: %v", err) + } + defer trace.Stop() } - defer keyLog.Close() - ctx := context.Background() + fmt.Printf("target server: %s\n", *servAddr) + tlsConf := &tls.Config{ InsecureSkipVerify: true, NextProtos: []string{"quic-echo-example"}, - KeyLogWriter: keyLog, + } + if *keyLogFile != "" { + keyLog, err := os.OpenFile(*keyLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Fatal(err) + } + defer keyLog.Close() + tlsConf.KeyLogWriter = keyLog } + ctx := context.Background() /* - conn, err := quic.DialAddr(ctx, servAddr, tlsConf, nil) + conn, err := quic.DialAddr(ctx, *servAddr, tlsConf, nil) if err != nil { log.Fatal("Cannot dial QUIC server:", err) } defer conn.CloseWithError(0, "") */ - serverAddr, err := net.ResolveUDPAddr("udp", servAddr) + serverAddr, err := net.ResolveUDPAddr("udp", *servAddr) if err != nil { log.Fatal("ServerAddr resolution fail:", err) } @@ -56,6 +70,14 @@ func main() { defer listener.Close() conn, err := quic.Dial(ctx, listener, serverAddr, tlsConf, nil) + /* + cidGenerator := cid.NewDpvsQCID(10, 4, 0, nil, 0) + transport := &quic.Transport{ + Conn: listener, + ConnectionIDGenerator: cidGenerator, + } + conn, err := transport.Dial(ctx, serverAddr, tlsConf, nil) + */ if err != nil { log.Fatal("Cannot dial QUIC server:", err) } diff --git a/test/quic/http3/h3client/h3client.go b/test/quic/http3/h3client/h3client.go index a040d1204..246a23eb1 100644 --- a/test/quic/http3/h3client/h3client.go +++ b/test/quic/http3/h3client/h3client.go @@ -20,7 +20,7 @@ func main() { quiet := flag.Bool("q", false, "don't print the data") keyLogFile := flag.String("keylog", "", "key log file") insecure := flag.Bool("insecure", false, "skip certificate verification") - cert := flag.String("cert", "../certs/cert.pem", "TLS certificate") + cert := flag.String("cert", "", "TLS certificate") flag.Parse() urls := flag.Args() diff --git a/test/quic/http3/h3server/h3server.go b/test/quic/http3/h3server/h3server.go index c739bdb90..1fd729edc 100644 --- a/test/quic/http3/h3server/h3server.go +++ b/test/quic/http3/h3server/h3server.go @@ -137,8 +137,8 @@ func main() { flag.Var(&bs, "bind", "bind to") www := flag.String("www", "", "www data") tcp := flag.Bool("tcp", false, "also listen on TCP") - key := flag.String("key", "../certs/key.pem", "TLS key (requires -cert option)") - cert := flag.String("cert", "../certs/cert.pem", "TLS certificate (requires -key option)") + key := flag.String("key", "./http3/certs/key.pem", "TLS key (requires -cert option)") + cert := flag.String("cert", "./http3/certs/cert.pem", "TLS certificate (requires -key option)") flag.Parse() if len(bs) == 0 { diff --git a/test/quic/pkg/cid/cid_generator.go b/test/quic/pkg/cid/cid_generator.go index 1a99db813..90307410c 100644 --- a/test/quic/pkg/cid/cid_generator.go +++ b/test/quic/pkg/cid/cid_generator.go @@ -5,6 +5,8 @@ import ( "fmt" "io" "net" + + quic "github.com/quic-go/quic-go" ) const ( @@ -12,22 +14,74 @@ const ( DPVS_QUIC_DCID_BYTES_MIN = 7 ) -func QuicCIDGenerator( +type DpvsQCID struct { + cidLen uint8 + l3len uint8 + l4len uint8 + svrIP net.IP + svrPort uint16 +} + +var _ quic.ConnectionIDGenerator = (*DpvsQCID)(nil) + +func NewDpvsQCID(cidLen, l3len, l4len uint8, + svrIP net.IP, svrPort uint16) *DpvsQCID { + if cidLen < DPVS_QUIC_DCID_BYTES_MIN { + cidLen = DPVS_QUIC_DCID_BYTES_MIN + } + if l3len < 1 { + l3len = 1 + } else if l3len > 8 { + l3len = 8 + } + if l4len > 0 { + l4len = 2 + } + if svrIP == nil { + svrIP, _ = FindLocalIP("") + } + + return &DpvsQCID{ + cidLen: cidLen, + l3len: l3len, + l4len: l4len, + svrIP: svrIP, + svrPort: svrPort, + } +} + +func (dqcid *DpvsQCID) ConnectionIDLen() int { + return int(dqcid.cidLen) +} + +func (dqcid *DpvsQCID) GenerateConnectionID() (quic.ConnectionID, error) { + data, err := QuicCIDGeneratorFunction(dqcid.cidLen, dqcid.l3len, + dqcid.l4len, dqcid.svrIP, dqcid.svrPort) + if err != nil { + data = make([]byte, dqcid.cidLen) + rand.Read(data[:]) + } + return quic.ConnectionIDFromBytes(data), err +} + +func QuicCIDGeneratorFunction( cidLen uint8, // the total length of CID to be generated, 7~20 bytes l3len uint8, // the length of server IP to encode into CID, 1~8 bytes l4len uint8, // the length of server Port to encode into CID, 0 or 2 bytes svrIP net.IP, // the server IP svrPort uint16, // the server Port -) (error, []byte) { +) ([]byte, error) { rdbuf := make([]byte, QUIC_CID_BUF_LEN) var i uint8 var l3addr []byte var l4addr uint16 - if cidLen < DPVS_QUIC_DCID_BYTES_MIN || l3len > 8 || l3len < 1 || + if svrIP == nil || + cidLen < DPVS_QUIC_DCID_BYTES_MIN || + l3len > 8 || l3len < 1 || (l4len != 0 && l4len != 2) || cidLen < l3len+l4len+5 { - return fmt.Errorf("invalid params"), nil + return nil, fmt.Errorf("invalid params") } entropy := cidLen - l3len - l4len + 1 @@ -42,14 +96,14 @@ func QuicCIDGenerator( } else { ipbytes = svrIP.To16() if ipbytes == nil { - return fmt.Errorf("invalid IP %v", svrIP), nil + return nil, fmt.Errorf("invalid IP %v", svrIP) } l3addr = ipbytes[16-l3len:] } l4addr = svrPort if _, err := io.ReadFull(rand.Reader, rdbuf[:entropy]); err != nil { - return err, nil + return nil, err } cid := make([]byte, cidLen, cidLen) @@ -73,5 +127,5 @@ func QuicCIDGenerator( cid[l3len+l4len+1] |= (rdbuf[1] & 0xf) copy(cid[l3len+l4len+2:], rdbuf[2:entropy-1]) - return nil, cid + return cid, nil } diff --git a/test/quic/pkg/cid/cid_generator_test.go b/test/quic/pkg/cid/cid_generator_test.go index d527a868f..652606ed5 100644 --- a/test/quic/pkg/cid/cid_generator_test.go +++ b/test/quic/pkg/cid/cid_generator_test.go @@ -7,7 +7,7 @@ import ( ) func TestQuicCIDGenerator_IPv4(t *testing.T) { - err, cid := QuicCIDGenerator(10, 3, 2, net.ParseIP("192.168.111.222"), 8029) + cid, err := QuicCIDGeneratorFunction(10, 3, 2, net.ParseIP("192.168.111.222"), 8029) if err != nil { t.Errorf("QuicCIDGenerator error return: %v", err) } @@ -27,7 +27,8 @@ func TestQuicCIDGenerator_IPv4(t *testing.T) { } func TestQuicCIDGenerator_IPv6(t *testing.T) { - err, cid := QuicCIDGenerator(16, 6, 2, net.ParseIP("2001::123:4567:89ab:cdef"), 51321) + cid, err := QuicCIDGeneratorFunction(16, 6, 2, + net.ParseIP("2001::123:4567:89ab:cdef"), 51321) if err != nil { t.Errorf("QuicCIDGenerator error return: %v", err) } diff --git a/test/quic/pkg/cid/server_addr.go b/test/quic/pkg/cid/server_addr.go new file mode 100644 index 000000000..b1aab217a --- /dev/null +++ b/test/quic/pkg/cid/server_addr.go @@ -0,0 +1,25 @@ +package cid + +import ( + "net" +) + +func FindLocalIP(targetIP string) (net.IP, error) { + if len(targetIP) == 0 { + targetIP = "8.8.8.8" + } + + raddr, err := net.ResolveIPAddr("ip", targetIP) + if err != nil { + return nil, err + } + + conn, err := net.DialIP("ip:icmp", nil, raddr) + if err != nil { + return nil, err + } + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.IPAddr) + return localAddr.IP, nil +} diff --git a/test/quic/server/quic-server.go b/test/quic/server/quic-server.go index 27c34dbe4..690f044c6 100644 --- a/test/quic/server/quic-server.go +++ b/test/quic/server/quic-server.go @@ -8,43 +8,58 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "flag" "fmt" "log" "math/big" "net" "os" + "syscall" quic "github.com/quic-go/quic-go" + "quic-test/pkg/cid" "quic-test/pkg/uoa" ) -var servAddr = ":4242" +var ( + hostIP net.IP +) -var keyLogFile = "quic-go-server-sshkey.log" +func init() { + if hostIP, _ = cid.FindLocalIP(""); hostIP == nil { + hostIP = net.IPv4(127, 0, 0, 1) + } + fmt.Println("Host IP:", hostIP) +} func main() { - if len(os.Args) > 1 { - servAddr = os.Args[1] - } - fmt.Printf("Quic Server listens on %s\n", servAddr) + servAddr := flag.String("server", ":4242", "server listener address") + keyLogFile := flag.String("keylog", "", "key log file") + uoaCliAddr := flag.Bool("uoa", true, "enable uoa client address") + flag.Parse() - keyLog, err := os.Create(keyLogFile) - if err != nil { - log.Fatal(err) - } - defer keyLog.Close() + fmt.Printf("Quic Server listens on %s (uoa client address %v)\n", *servAddr, *uoaCliAddr) tlsConf := generateTLSConfig() - tlsConf.KeyLogWriter = keyLog + if *keyLogFile != "" { + keyLog, err := os.Create(*keyLogFile) + if err != nil { + log.Fatal(err) + } + defer keyLog.Close() + tlsConf.KeyLogWriter = keyLog + } + + cidGenerator := cid.NewDpvsQCID(10, 4, 0, hostIP, 0) /* - listener, err := quic.ListenAddr(servAddr, tlsConf, nil) + listener, err := quic.ListenAddr(*servAddr, tlsConf, nil) if err != nil { panic(err) } */ - udpAddr, err := net.ResolveUDPAddr("udp", servAddr) + udpAddr, err := net.ResolveUDPAddr("udp", *servAddr) if err != nil { panic(err) } @@ -53,34 +68,54 @@ func main() { panic(err) } listener, err := (&quic.Transport{ - Conn: udpConn, + Conn: udpConn, + ConnectionIDGenerator: cidGenerator, }).Listen(tlsConf, nil) if err != nil { panic(err) } defer listener.Close() + var uoaConn *net.UDPConn + if *uoaCliAddr { + uoaConn = udpConn + } + ctx := context.Background() for { sess, err := listener.Accept(ctx) if err != nil { panic(err) } - go handleSession(ctx, udpConn, sess) + go handleSession(ctx, uoaConn, sess) } } func handleSession(ctx context.Context, udpConn *net.UDPConn, sess quic.Connection) { - file, err := udpConn.File() - if err != nil { - panic(err) - } - uoaAddr, err := uoa.GetUoaAddr(file.Fd(), sess.RemoteAddr(), sess.LocalAddr()) - if err != nil { - fmt.Printf("New connection from %v, uoaAddr failed for %v\n", sess.RemoteAddr(), err) + if udpConn != nil { + file, err := udpConn.File() + if err != nil { + panic(err) + } + defer file.Close() + + // FIXME: Even though the file is an duplicate from the original udpConn. + // a just single call to file.Fd() blocks the quic session noticeably when + // using the default blocking mode. Having no idea about the cause of this + // problem, just set the fd to be nonblock, hoping without other influences. + fd := file.Fd() + syscall.SetNonblock(int(fd), true) + + uoaAddr, err := uoa.GetUoaAddr(fd, sess.RemoteAddr(), sess.LocalAddr()) + if err != nil { + fmt.Printf("New connection from %v, uoaAddr failed for %v\n", sess.RemoteAddr(), err) + } else { + fmt.Printf("New connection from %v, uoaAddr %v\n", sess.RemoteAddr(), uoaAddr) + } } else { - fmt.Printf("New connection from %v, uoaAddr %v\n", sess.RemoteAddr(), uoaAddr) + fmt.Printf("New connection from %v\n", sess.RemoteAddr()) } + stream, err := sess.AcceptStream(ctx) if err != nil { panic(err) From 475a638335975664eb5f24bf11c06c43a85952c7 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 10 May 2024 18:11:22 +0800 Subject: [PATCH 32/63] ipvs: support quic connection migration Signed-off-by: ywc689 --- include/conf/service.h | 1 + include/ipvs/quic.h | 181 +++++++++++++ include/ipvs/service.h | 1 + src/ipvs/ip_vs_conhash.c | 8 + src/ipvs/ip_vs_proto_udp.c | 29 ++- src/ipvs/ip_vs_quic.c | 240 ++++++++++++++++++ ...1-quic-test-codes-for-conn-migration.patch | 63 +++++ tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go | 5 + tools/dpvs-agent/dpvs-agent-api.yaml | 8 + .../models/virtual_server_spec_expand.go | 50 ++++ .../models/virtual_server_spec_tiny.go | 3 + tools/dpvs-agent/pkg/ipc/types/const.go | 1 + tools/dpvs-agent/pkg/ipc/types/getmodel.go | 6 + .../dpvs-agent/pkg/ipc/types/virtualserver.go | 4 + tools/dpvs-agent/restapi/embedded_spec.go | 22 ++ tools/ipvsadm/ipvsadm.c | 12 +- .../keepalived/keepalived/check/check_data.c | 2 + .../keepalived/check/check_parser.c | 8 + .../keepalived/keepalived/check/ipvswrapper.c | 4 + tools/keepalived/keepalived/check/ipwrapper.c | 1 + .../keepalived/include/check_data.h | 2 + 21 files changed, 647 insertions(+), 4 deletions(-) create mode 100644 include/ipvs/quic.h create mode 100644 src/ipvs/ip_vs_quic.c create mode 100644 test/quic/0001-quic-test-codes-for-conn-migration.patch diff --git a/include/conf/service.h b/include/conf/service.h index d16164f3c..6eb5dd86b 100644 --- a/include/conf/service.h +++ b/include/conf/service.h @@ -42,6 +42,7 @@ #define IP_VS_SVC_F_SIP_HASH 0x0100 /* sip hash target */ #define IP_VS_SVC_F_QID_HASH 0x0200 /* quic cid hash target */ #define IP_VS_SVC_F_MATCH 0x0400 /* snat match */ +#define IP_VS_SVC_F_QUIC 0x0800 /* quic/h3 protocol */ #define IP_VS_SVC_F_SCHED_SH_FALLBACK IP_VS_SVC_F_SCHED1 /* SH fallback */ #define IP_VS_SVC_F_SCHED_SH_PORT IP_VS_SVC_F_SCHED2 /* SH use port */ diff --git a/include/ipvs/quic.h b/include/ipvs/quic.h new file mode 100644 index 000000000..767d9ebc1 --- /dev/null +++ b/include/ipvs/quic.h @@ -0,0 +1,181 @@ +/* + * DPVS is a software load balancer (Virtual Server) based on DPDK. + * + * Copyright (C) 2021 iQIYI (www.iqiyi.com). + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +#ifndef __DPVS_QUIC_H__ +#define __DPVS_QUICH__ + +#include +#include "ipvs/service.h" +#include "conf/inet.h" + +/* + * In order to support QUIC connection migration, DPVS makes an agreement on + * the format of QUIC Connection ID(CID) into which backend address information + * is encoded. Specifically, backend server should generate its QUIC CIDs complying + * with the format defined as below. + * + * DPVS QUIC Connction ID Format { + * First Octet (8), + * L3 Address Length (3), + * L4 Address Flag (1), + * L3 Address (8...64), + * [ L4 Address (16) ] + * Nonce (32...140) + * } + * + * The notations in CID format definition follows the RFC 9000 name notational + * convention. For detailed explanation, please refer to + * https://datatracker.ietf.org/doc/html/rfc9000#name-notational-conventions. + * + * First Octet: 8 bits + * Allows for compatibility with ITEF QUIC-LB drafts. Not used in DPVS. + * https://datatracker.ietf.org/doc/html/draft-ietf-quic-load-balancers-19 + * L3 Address Length: 3 bits + * The length of L3 Address in byte. Add 1 to the 3-bit value gets the actual + * length, which is in range 1...8. + * If the length less than legitimated length, i.e. 4 bytes for IPv4, 16 bytes + * for IPv6, the higher address bytes are truncated. + * L4 Address Flags: 1 bit + * Indicate whether L4 Address is included in this CID. + * 1 - L4 Address is included + * 0 - L4 Address is not included + * L3 Address: 8, 16, 24, 32, 40, 48, 56, 64 bits + * IPv4/IPv6 address with high bytes trimmed if necessary. + * Its length is specified by L3 Address Length. + * L4 Address: 16 bits, optional + * UDP port number. + * Nonce: 32 ~ 140 bits, and constrained by CID's max length of 160 bits + * This is server independent field, often filled with data generated randomly. + * A minimum length is 32 bits to satisfy the entropy requirement of QUIC protocol. + * + * DPVS QUIC CID adopts a variable-length code style. The server information takes + * a fixed 4-bit for address length, and a variable 8 ~ 48 bits for L3 and L4 addresses. + * DPVS may not take the whole L3/L4 Address into CID to reduce the CID length. For example, + * if all backend server are in private network cidr 192.168.0.0/16 listening on the same + * server port, then the use of lowest 16-bit L3 Address without L4 Address is appropriate. + * + * Note the server info in QUIC CID is not encrypted, and we don't plan to implement a quic + * server id allocator as required in IETF QUIC-LB drafts. This is just a simple, stateless + * and clear text encoding, which may subject to security vulnerability that can be exploited + * by an external observer to corelate CIDs of a QUIC connection easier. + */ + +#define DPVS_QUIC_DCID_BYTES_MIN 7 + +struct quic_server { + uint16_t wildcard; // enum value: 8, 16, 24, 32, 40, 48, 56, 64 + uint16_t port; // network endian + union inet_addr addr; +}; + +// Generate a Quic CID accepted by DPVS. The function demos an implementation +// for CID generator that may be used by Quic server applications on RS. +// +// For example, given +// cidlen: 10, l3len:2, l4len:2, +// svr_ip:192.168.111.222(0xC0A86FDE), svr_port:8029(0x1F5D) +// the function generator Quic CIDs like +// XX36 FDE1 F5DX XXXX XXXX +// where 'X' denotes a random hexadecimal. +// +// Params: +// af: l3 address family, valid values are (AF_INET, AF_INET6) +// cidlen: the expected cid total length in bytes, no less than DPVS_QUIC_DCID_BYTES_MIN +// l3len: length in bytes of l3 address to be encoded in cid, valid values are integers (1...8) +// l4len: length in bytes of l4 address to be encoded in cid, valid values are (0, 2) +// svr_ip: l3 address +// svr_port: l4 address +// cid: the result cid buffer, the buffer size must be no less than cidlen +static inline int quic_cid_generator(int af, int cidlen, + int l3len, int l4len, const union inet_addr *svr_ip, + uint16_t svr_port, char *cid) { + char rdbuf[20]; + int i, fd, ret, entropy, l4flag; + char *l3addr; + uint16_t l4addr; + + entropy = cidlen - l3len - l4len + 1; + l4flag = l4len > 0 ? 1 : 0; + if (AF_INET == af) + l3addr = (char *)svr_ip + (4 - l3len); + else + l3addr = (char *)svr_ip + (16 - l3len); + l4addr = svr_port; + + if (cidlen < DPVS_QUIC_DCID_BYTES_MIN || + l3len > 8 || l3len < 1 || + (l4len != 0 && l4len != 2) || + cidlen < l3len + l4len + 5) + return -1; + fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) + return -1; + ret = read(fd, rdbuf, entropy); + if (ret != entropy) + return -1; + + cid[0] = rdbuf[0]; + cid[1] = (((l3len - 1) & 0x7) << 5) + | ((l4flag & 0x1) << 4) + | ((*l3addr>> 4) & 0xf); + for (i = 0; i < l3len; i++) { + if (i == l3len - 1) + cid[2+i] = ((*l3addr & 0xf) << 4); + else + cid[2+i] = ((*l3addr & 0xf) << 4) | ((*(l3addr+1) >> 4) & 0xf); + l3addr++; + } + if (l4len > 0) { + cid[l3len+1] &= 0xf0; + cid[l3len+1] |= ((l4addr >> 12) & 0xf); + l4addr <<= 4; + cid[l3len+2] = (l4addr >> 8) & 0xff; + cid[l3len+3] = l4addr & 0xff; + } + cid[l3len+l4len+1] |= (rdbuf[1] & 0xf); + memcpy(&cid[l3len+l4len+2], &rdbuf[2], entropy - 3); + return 0; +} + +static inline void quic_dump_server(const struct quic_server *qsvr, + char *buf, int bufsize) { + int af; + char addrbuf[64] = { 0 }; + + buf[0] = '\0'; + af = qsvr->wildcard > 32 ? AF_INET6 : AF_INET; // an approximation, not accurate + if (NULL == inet_ntop(af, &qsvr->addr, addrbuf, sizeof(addrbuf))) + return; + if (AF_INET == af) + snprintf(buf, bufsize, "%s:%d", addrbuf, ntohs(qsvr->port)); + else + snprintf(buf, bufsize, "[%s]:%d", addrbuf, ntohs(qsvr->port)); +} + +// Parse backend server address information from mbuf into qsvr. +int quic_parse_server(const struct rte_mbuf *, + const struct dp_vs_iphdr *, + struct quic_server *); + +// Schedule a dpvs conn using the backend server specified by qsvr. +// Return NULL if the backend server doesn't exists in the svc's rs list. +struct dp_vs_conn* quic_schedule(const struct dp_vs_service *, + const struct quic_server *, + const struct dp_vs_iphdr *, + struct rte_mbuf *); + +#endif diff --git a/include/ipvs/service.h b/include/ipvs/service.h index a4cbff8e8..67c282e2b 100644 --- a/include/ipvs/service.h +++ b/include/ipvs/service.h @@ -45,6 +45,7 @@ #define DP_VS_SVC_F_SIP_HASH IP_VS_SVC_F_SIP_HASH #define DP_VS_SVC_F_QID_HASH IP_VS_SVC_F_QID_HASH #define DP_VS_SVC_F_MATCH IP_VS_SVC_F_MATCH +#define DP_VS_SVC_F_QUIC IP_VS_SVC_F_QUIC /* virtual service */ struct dp_vs_service { diff --git a/src/ipvs/ip_vs_conhash.c b/src/ipvs/ip_vs_conhash.c index 360346ad2..ca68a43b9 100644 --- a/src/ipvs/ip_vs_conhash.c +++ b/src/ipvs/ip_vs_conhash.c @@ -39,6 +39,14 @@ struct conhash_sched_data { /* * QUIC CID hash target for quic* * QUIC CID(qid) should be configured in UDP service + * + * This is an early Google QUIC implementation, and has been obsoleted. + * https://docs.google.com/document/d/1WJvyZflAO2pq77yOLbp9NsGjC1CHetAXV8I0fQe-B_U/edit?pli=1#heading=h.o9jvitkc5d2g + * + * Use IETF QUIC(officially published in 2021) instead. + * Configure `--quic` option on DPVS service to enable it. + * The quic application on RS must conform with the CID format agreement + * declared in `include/ipvs/quic.h`. */ static int get_quic_hash_target(int af, const struct rte_mbuf *mbuf, uint64_t *quic_cid) diff --git a/src/ipvs/ip_vs_proto_udp.c b/src/ipvs/ip_vs_proto_udp.c index a3f2a4690..df58fc924 100644 --- a/src/ipvs/ip_vs_proto_udp.c +++ b/src/ipvs/ip_vs_proto_udp.c @@ -26,6 +26,7 @@ #include "ipvs/ipvs.h" #include "ipvs/proto.h" #include "ipvs/proto_udp.h" +#include "ipvs/quic.h" #include "ipvs/conn.h" #include "ipvs/service.h" #include "ipvs/blklst.h" @@ -174,10 +175,32 @@ static int udp_conn_sched(struct dp_vs_proto *proto, } /* schedule RS and create new connection */ - *conn = dp_vs_schedule(svc, iph, mbuf, false); + *conn = NULL; + if (svc->flags & DP_VS_SVC_F_QUIC) { // deal with quic conn migration + struct quic_server qsvr = { 0 }; + int err = quic_parse_server(mbuf, iph, &qsvr); + if (likely(err == EDPVS_OK)) { + if (qsvr.wildcard > 0) { + *conn = quic_schedule(svc, &qsvr, iph, mbuf); + if (*conn) + RTE_LOG(INFO, IPVS, "schedule new connection from quic cid\n"); + else { + // Do NOT emit warning log here! + // The DCID in Initial packets are generated randomly by client, which + // doesn't contain valid server address info for success schedule. + } + } + } else { + RTE_LOG(WARNING, IPVS, "fail to parse server info from quic mbuf: %s\n", + dpvs_strerror(err)); + } + } if (!*conn) { - *verdict = INET_DROP; - return EDPVS_RESOURCE; + *conn = dp_vs_schedule(svc, iph, mbuf, false); + if (!*conn) { + *verdict = INET_DROP; + return EDPVS_RESOURCE; + } } if ((*conn)->dest->fwdmode == DPVS_FWD_MODE_FNAT && g_uoa_max_trail > 0) { diff --git a/src/ipvs/ip_vs_quic.c b/src/ipvs/ip_vs_quic.c new file mode 100644 index 000000000..69e9118ab --- /dev/null +++ b/src/ipvs/ip_vs_quic.c @@ -0,0 +1,240 @@ +/* + * DPVS is a software load balancer (Virtual Server) based on DPDK. + * + * Copyright (C) 2021 iQIYI (www.iqiyi.com). + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "ipvs/quic.h" +#include "ipvs/conn.h" + +/* + * quic_parse_server extract encoded RS info from DCID in quic packet header. + * Note there exists two header types in quic. + * (https://datatracker.ietf.org/doc/html/rfc9000#name-packet-formats) + * + * Long Header Packet { + * Header Form (1) = 1, + * Fixed Bit (1) = 1, + * Long Packet Type (2), + * Type-Specific Bits (4), + * Version (32), + * Destination Connection ID Length (8), + * Destination Connection ID (0..160), + * Source Connection ID Length (8), + * Source Connection ID (0..160), + * Type-Specific Payload (..), + * } + * + * 1-RTT Packet { + * Header Form (1) = 0, + * Fixed Bit (1) = 1, + * Spin Bit (1), + * Reserved Bits (2), + * Key Phase (1), + * Packet Number Length (2), + * Destination Connection ID (0..160), + * Packet Number (8..32), + * Packet Payload (8..), + * } + */ +union quic_header { + struct { +#if defined(__LITTLE_ENDIAN_BITFIELD) || (__BYTE_ORDER == __LITTLE_ENDIAN) + unsigned int typedata:4; + unsigned int type:2; + unsigned int fixed:1; + unsigned int form:1; +#elif defined (__BIG_ENDIAN_BITFIELD) || (__BYTE_ORDER == __BIG_ENDIAN) + unsigned int form:1; + unsigned int fixed:1; + unsigned int type:2; + unsigned int typedata:4; +#else +#error "Please fix " +#endif + unsigned char extra[0]; // version, DCID len, DCID, SCID len, SCID, packet number, payload + } lhdr; + struct { +#if defined(__LITTLE_ENDIAN_BITFIELD) || (__BYTE_ORDER == __LITTLE_ENDIAN) + unsigned int pkt_num_len:2; + unsigned int key_phase:1; + unsigned int reserved:2; + unsigned int spin:1; + unsigned int fixed:1; + unsigned int form:1; +#elif defined (__BIG_ENDIAN_BITFIELD) || (__BYTE_ORDER == __BIG_ENDIAN) + unsigned int form:1; + unsigned int fixed:1; + unsigned int spin:1; + unsigned int reserved:2; + unsigned int key_phase:1; + unsigned int pkt_num_len:2; +#else +#error "Please fix " +#endif + unsigned char extra[0]; // DCID, packet number, payload + } shdr; +}; + + +static inline bool quic_server_match(const struct quic_server *qsvr, + const struct dp_vs_dest *dest) { + int l3len; + const unsigned char *ptr1, *ptr2; + + if (unlikely(!qsvr->wildcard || qsvr->wildcard % 8)) + return false; + l3len = qsvr->wildcard >> 3; + + if (AF_INET == dest->af) { + ptr1 = ((const unsigned char *)&dest->addr) + (4 - l3len); + ptr2 = ((const unsigned char *)&qsvr->addr) + (4 - l3len); + } else { + ptr1 = ((const unsigned char *)&dest->addr) + (16 - l3len); + ptr2 = ((const unsigned char *)&qsvr->addr) + (16 - l3len); + } + + while (l3len-- > 0) { + if (*ptr1 != *ptr2) + return false; + ptr1++; + ptr2++; + } + + if (!qsvr->port) + return true; + return qsvr->port == dest->port; +} + +int quic_parse_server(const struct rte_mbuf *mbuf, + const struct dp_vs_iphdr *iph, + struct quic_server *qsvr) { + int offset = iph->len; + int i, l3len, l4len; + unsigned char *ptr, *dptr; + + union quic_header *qhdr, hdrbuf; + uint32_t *qver, qverbuf; + uint8_t *cidlen, cidlenbuf; + unsigned char *cid, cidbuf[20]; + + memset(qsvr, 0, sizeof(struct quic_server)); + + offset += sizeof(struct rte_udp_hdr); + qhdr = mbuf_header_pointer(mbuf, offset, sizeof(hdrbuf), &hdrbuf); + if (unlikely(!qhdr)) + return EDPVS_INVPKT; + + if (unlikely(!qhdr->lhdr.fixed)) + return EDPVS_INVPKT; + + offset++; + if (qhdr->lhdr.form) { // quic long header + qver = mbuf_header_pointer(mbuf, offset, sizeof(qverbuf), &qverbuf); + if (unlikely(NULL == qver || ntohl(*qver) > 1)) + return EDPVS_INVPKT; + offset += sizeof(qverbuf); + cidlen = mbuf_header_pointer(mbuf, offset, sizeof(cidlenbuf), &cidlenbuf); + if (unlikely(NULL == cidlen || *cidlen > 20)) + return EDPVS_INVPKT; + if (*cidlen < DPVS_QUIC_DCID_BYTES_MIN) + return EDPVS_OK; // possible conn without DCID, or cilient Initial packets + offset += sizeof(cidlenbuf); + cid = mbuf_header_pointer(mbuf, offset, *cidlen, &cidbuf); + if (unlikely(!cid)) + return EDPVS_INVPKT; + } else { // quic short header + cid = mbuf_header_pointer(mbuf, offset, DPVS_QUIC_DCID_BYTES_MIN, &cidbuf); + if (NULL == cid) + return EDPVS_OK; // possible conn without DCID + ptr = cid + 1; + cidlen = &cidlenbuf; + *cidlen = ((*ptr >> 5) & 0x3) + 1; + if (*ptr & 0x10) + *cidlen += 2; + *cidlen += 6; + if (*cidlen > DPVS_QUIC_DCID_BYTES_MIN) { + cid = mbuf_header_pointer(mbuf, offset, *cidlen, &cidbuf); + if (unlikely(!cid)) + return EDPVS_OK; // possible conn without DCID + } + } + + ptr = cid; + ++ptr; // skip first octet + l3len = ((*ptr >> 5) & 0x7) + 1; + l4len = (*ptr & 0x10) ? 2 : 0; + + qsvr->wildcard = l3len << 3; + + if (AF_INET == iph->af) + dptr = ((unsigned char *)&qsvr->addr) + (4 - l3len); + else + dptr = ((unsigned char *)&qsvr->addr) + (16 - l3len); + + for (i = 0; i < l3len; i++, ptr++, dptr++) + *dptr = ((*ptr & 0xf) << 4) | ((*(ptr+1) >> 4) & 0xf); + + if (l4len) { + dptr = (unsigned char *)&qsvr->port; + for (i = 0; i < l4len; i++, ptr++, dptr++) + *dptr = ((*ptr & 0xf) << 4) | ((*(ptr+1) >> 4) & 0xf); + } + + return EDPVS_OK; +} + +struct dp_vs_conn* quic_schedule(const struct dp_vs_service *svc, + const struct quic_server *qsvr, + const struct dp_vs_iphdr *iph, + struct rte_mbuf *mbuf) { + bool found = false; + struct dp_vs_dest *dest; + uint16_t _ports[2], *ports; + uint32_t flags = 0; + struct dp_vs_conn_param param; + struct dp_vs_conn *conn; + + if (unlikely(!qsvr || !qsvr->wildcard || iph->proto != IPPROTO_UDP + || svc->flags & DP_VS_SVC_F_PERSISTENT)) + return NULL; + + list_for_each_entry(dest, &svc->dests, n_list) { + if (quic_server_match(qsvr, dest)) { + found = true; + break; + } + } + if (!found || dest->fwdmode == DPVS_FWD_MODE_SNAT) + return NULL; + + ports = mbuf_header_pointer(mbuf, iph->len, sizeof(_ports), _ports); + if (unlikely(!ports)) + return NULL; + dp_vs_conn_fill_param(iph->af, iph->proto, + &iph->saddr, &iph->daddr, + ports[0], ports[1], + 0, ¶m); + + if (svc->flags & DP_VS_SVC_F_EXPIRE_QUIESCENT) + flags |= DP_VS_SVC_F_EXPIRE_QUIESCENT; + + conn = dp_vs_conn_new(mbuf, iph, ¶m, dest, flags); + if (!conn) + return NULL; + + dp_vs_stats_conn(conn); + return conn; +} diff --git a/test/quic/0001-quic-test-codes-for-conn-migration.patch b/test/quic/0001-quic-test-codes-for-conn-migration.patch new file mode 100644 index 000000000..a83f2e7c6 --- /dev/null +++ b/test/quic/0001-quic-test-codes-for-conn-migration.patch @@ -0,0 +1,63 @@ +From 462d64d15bed454a7fc1a367490910018b583003 Mon Sep 17 00:00:00 2001 +From: ywc689 +Date: Wed, 15 May 2024 10:33:44 +0800 +Subject: [PATCH] quic test codes for conn migration + +Signed-off-by: ywc689 +--- + src/ipvs/ip_vs_core.c | 14 ++++++++++++++ + src/ipvs/ip_vs_proto_udp.c | 6 ++++++ + 2 files changed, 20 insertions(+) + +diff --git a/src/ipvs/ip_vs_core.c b/src/ipvs/ip_vs_core.c +index b92e984..08c2479 100644 +--- a/src/ipvs/ip_vs_core.c ++++ b/src/ipvs/ip_vs_core.c +@@ -38,6 +38,7 @@ + #include "ipvs/proto_udp.h" + #include "route6.h" + #include "ipvs/redirect.h" ++#include "ipvs/quic.h" + + static inline int dp_vs_fill_iphdr(int af, struct rte_mbuf *mbuf, + struct dp_vs_iphdr *iph) +@@ -990,6 +991,19 @@ static int __dp_vs_in(void *priv, struct rte_mbuf *mbuf, + return INET_DROP; + } + ++ // TODO: remove the test codes ++ if (iph.proto == IPPROTO_UDP && conn && dir == DPVS_CONN_DIR_INBOUND) { ++ int err; ++ struct quic_server qsvr; ++ char buf[256]; ++ ++ err = quic_parse_server(mbuf, &iph, &qsvr); ++ if (err == EDPVS_OK) { ++ quic_dump_server(&qsvr, buf, sizeof(buf)); ++ RTE_LOG(INFO, IPVS, "*** got quic server in mbuf: %s ***\n", buf); ++ } ++ } ++ + /* + * The connection is not locally found, however the redirect is found so + * forward the packet to the remote redirect owner core. +diff --git a/src/ipvs/ip_vs_proto_udp.c b/src/ipvs/ip_vs_proto_udp.c +index df58fc9..73a88bf 100644 +--- a/src/ipvs/ip_vs_proto_udp.c ++++ b/src/ipvs/ip_vs_proto_udp.c +@@ -180,6 +180,12 @@ static int udp_conn_sched(struct dp_vs_proto *proto, + struct quic_server qsvr = { 0 }; + int err = quic_parse_server(mbuf, iph, &qsvr); + if (likely(err == EDPVS_OK)) { ++ // TODO: remove the test codes ++ { ++ qsvr.wildcard = 16; ++ qsvr.addr.in.s_addr = htonl(0x581e); //x.x.88.30 ++ qsvr.port = htons(4141); ++ } + if (qsvr.wildcard > 0) { + *conn = quic_schedule(svc, &qsvr, iph, mbuf); + if (*conn) +-- +1.8.3.1 + diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go index b040e2074..52328290d 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port.go @@ -70,6 +70,10 @@ func (h *putVsItem) Handle(params apiVs.PutVsVipPortParams) middleware.Responder vs.SetFlagsExpireQuiescent() } + if params.Spec.Quic != nil && *params.Spec.Quic { + vs.SetFlagsQuic() + } + if params.Spec.SynProxy != nil && *params.Spec.SynProxy { vs.SetFlagsSynProxy() } @@ -123,6 +127,7 @@ func (h *putVsItem) Handle(params apiVs.PutVsVipPortParams) middleware.Responder vsModel.ConnTimeout = newVsModel.ConnTimeout vsModel.LimitProportion = newVsModel.LimitProportion vsModel.ExpireQuiescent = newVsModel.ExpireQuiescent + vsModel.Quic = newVsModel.Quic vsModel.Fwmark = newVsModel.Fwmark vsModel.SynProxy = newVsModel.SynProxy vsModel.Match = newVsModel.Match diff --git a/tools/dpvs-agent/dpvs-agent-api.yaml b/tools/dpvs-agent/dpvs-agent-api.yaml index f06d79c92..8840cdafd 100644 --- a/tools/dpvs-agent/dpvs-agent-api.yaml +++ b/tools/dpvs-agent/dpvs-agent-api.yaml @@ -555,6 +555,11 @@ definitions: enum: - "true" - "false" + Quic: + type: "string" + enum: + - "true" + - "false" Timeout: type: "integer" format: "uint32" @@ -603,6 +608,9 @@ definitions: ExpireQuiescent: type: "boolean" default: false + Quic: + type: "boolean" + default: false Timeout: type: "integer" format: "uint32" diff --git a/tools/dpvs-agent/models/virtual_server_spec_expand.go b/tools/dpvs-agent/models/virtual_server_spec_expand.go index ce917ce61..21482cc6e 100644 --- a/tools/dpvs-agent/models/virtual_server_spec_expand.go +++ b/tools/dpvs-agent/models/virtual_server_spec_expand.go @@ -70,6 +70,10 @@ type VirtualServerSpecExpand struct { // Enum: [0 1 2 17 18] ProxyProto uint8 `json:"ProxyProto,omitempty"` + // quic + // Enum: [true false] + Quic string `json:"Quic,omitempty"` + // r ss RSs *RealServerExpandList `json:"RSs,omitempty"` @@ -111,6 +115,10 @@ func (m *VirtualServerSpecExpand) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateQuic(formats); err != nil { + res = append(res, err) + } + if err := m.validateRSs(formats); err != nil { res = append(res, err) } @@ -248,6 +256,48 @@ func (m *VirtualServerSpecExpand) validateProxyProto(formats strfmt.Registry) er return nil } +var virtualServerSpecExpandTypeQuicPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["true","false"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + virtualServerSpecExpandTypeQuicPropEnum = append(virtualServerSpecExpandTypeQuicPropEnum, v) + } +} + +const ( + + // VirtualServerSpecExpandQuicTrue captures enum value "true" + VirtualServerSpecExpandQuicTrue string = "true" + + // VirtualServerSpecExpandQuicFalse captures enum value "false" + VirtualServerSpecExpandQuicFalse string = "false" +) + +// prop value enum +func (m *VirtualServerSpecExpand) validateQuicEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, virtualServerSpecExpandTypeQuicPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *VirtualServerSpecExpand) validateQuic(formats strfmt.Registry) error { + if swag.IsZero(m.Quic) { // not required + return nil + } + + // value enum + if err := m.validateQuicEnum("Quic", "body", m.Quic); err != nil { + return err + } + + return nil +} + func (m *VirtualServerSpecExpand) validateRSs(formats strfmt.Registry) error { if swag.IsZero(m.RSs) { // not required return nil diff --git a/tools/dpvs-agent/models/virtual_server_spec_tiny.go b/tools/dpvs-agent/models/virtual_server_spec_tiny.go index 0219c186d..543a417fa 100644 --- a/tools/dpvs-agent/models/virtual_server_spec_tiny.go +++ b/tools/dpvs-agent/models/virtual_server_spec_tiny.go @@ -42,6 +42,9 @@ type VirtualServerSpecTiny struct { // Enum: [v2 v2-insecure v1 v1-insecure disable] ProxyProtocol string `json:"ProxyProtocol,omitempty"` + // quic + Quic *bool `json:"Quic,omitempty"` + // sched name // Enum: [rr wrr wlc conhash] SchedName string `json:"SchedName,omitempty"` diff --git a/tools/dpvs-agent/pkg/ipc/types/const.go b/tools/dpvs-agent/pkg/ipc/types/const.go index 6cbf89f02..590b48d14 100644 --- a/tools/dpvs-agent/pkg/ipc/types/const.go +++ b/tools/dpvs-agent/pkg/ipc/types/const.go @@ -47,6 +47,7 @@ const ( DPVS_SVC_F_SIP_HASH // 0x100 DPVS_SVC_F_QID_HASH // 0x200 DPVS_SVC_F_MATCH // 0x400 + DPVS_SVC_F_QUIC // 0x800 ) const ( diff --git a/tools/dpvs-agent/pkg/ipc/types/getmodel.go b/tools/dpvs-agent/pkg/ipc/types/getmodel.go index a1d0e30f8..36be36731 100644 --- a/tools/dpvs-agent/pkg/ipc/types/getmodel.go +++ b/tools/dpvs-agent/pkg/ipc/types/getmodel.go @@ -31,6 +31,7 @@ func (vs *VirtualServerSpec) GetModel() *models.VirtualServerSpecExpand { Fwmark: vs.GetFwmark(), SynProxy: "false", ExpireQuiescent: "false", + Quic: "false", SchedName: vs.GetSchedName(), Timeout: vs.GetTimeout(), Match: vs.match.GetModel(), @@ -49,6 +50,11 @@ func (vs *VirtualServerSpec) GetModel() *models.VirtualServerSpecExpand { flags += "ExpireQuiescent|" } + if (vs.GetFlags() & DPVS_SVC_F_QUIC) != 0 { + modelVs.Quic = "true" + flags += "Quic|" + } + if (vs.GetFlags() & DPVS_SVC_F_QID_HASH) != 0 { flags += "ConHashByQuicID|" } diff --git a/tools/dpvs-agent/pkg/ipc/types/virtualserver.go b/tools/dpvs-agent/pkg/ipc/types/virtualserver.go index 1dc99f35a..e850946c9 100644 --- a/tools/dpvs-agent/pkg/ipc/types/virtualserver.go +++ b/tools/dpvs-agent/pkg/ipc/types/virtualserver.go @@ -289,6 +289,10 @@ func (vs *VirtualServerSpec) SetFlagsExpireQuiescent() { vs.setFlags(DPVS_SVC_F_EXPIRE_QUIESCENT) } +func (vs *VirtualServerSpec) SetFlagsQuic() { + vs.setFlags(DPVS_SVC_F_QUIC) +} + func (vs *VirtualServerSpec) SetFlagsPersistent() { vs.setFlags(DPVS_SVC_F_PERSISTENT) } diff --git a/tools/dpvs-agent/restapi/embedded_spec.go b/tools/dpvs-agent/restapi/embedded_spec.go index aef67a7fe..e16e252cf 100644 --- a/tools/dpvs-agent/restapi/embedded_spec.go +++ b/tools/dpvs-agent/restapi/embedded_spec.go @@ -1975,6 +1975,13 @@ func init() { 18 ] }, + "Quic": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, "RSs": { "$ref": "#/definitions/RealServerExpandList" }, @@ -2042,6 +2049,10 @@ func init() { "disable" ] }, + "Quic": { + "type": "boolean", + "default": false + }, "SchedName": { "type": "string", "enum": [ @@ -4517,6 +4528,13 @@ func init() { 18 ] }, + "Quic": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, "RSs": { "$ref": "#/definitions/RealServerExpandList" }, @@ -4584,6 +4602,10 @@ func init() { "disable" ] }, + "Quic": { + "type": "boolean", + "default": false + }, "SchedName": { "type": "string", "enum": [ diff --git a/tools/ipvsadm/ipvsadm.c b/tools/ipvsadm/ipvsadm.c index dd234691e..b61f38837 100644 --- a/tools/ipvsadm/ipvsadm.c +++ b/tools/ipvsadm/ipvsadm.c @@ -336,6 +336,7 @@ enum { TAG_DEST_CHECK, TAG_CONN_TIMEOUT, TAG_PROXY_PROTOCOL, + TAG_QUIC, }; /* various parsing helpers & parsing functions */ @@ -562,6 +563,7 @@ parse_options(int argc, char **argv, struct ipvs_command_entry *ce, { "dest-check", '\0', POPT_ARG_STRING, &optarg, TAG_DEST_CHECK, NULL, NULL}, { "conn-timeout", '\0', POPT_ARG_INT, &intarg, TAG_CONN_TIMEOUT, NULL, NULL}, { "proxy-protocol", '\0', POPT_ARG_STRING, &optarg, TAG_PROXY_PROTOCOL, NULL, NULL}, + { "quic", '\0', POPT_ARG_NONE, NULL, TAG_QUIC, NULL, NULL}, { NULL, 0, 0, NULL, 0, NULL, NULL } }; @@ -968,6 +970,11 @@ parse_options(int argc, char **argv, struct ipvs_command_entry *ce, ce->dpvs_svc.flags = ce->dpvs_svc.flags | IP_VS_SVC_F_EXPIRE_QUIESCENT; break; } + case TAG_QUIC: + { + ce->dpvs_svc.flags = ce->dpvs_svc.flags | IP_VS_SVC_F_QUIC; + break; + } case TAG_DEST_CHECK: { if (parse_dest_check(optarg, &ce->dpvs_svc.check_conf) != 0) { @@ -1733,7 +1740,8 @@ static void usage_exit(const char *program, const int exit_status) " DOWNONLY:=down_retry,down_wait, for example, --dest-check=1,3s\n" " --laddr -z local-ip local IP\n" " --blklst -k blacklist-ip blacklist IP for specific service\n" - " --whtlst -2 whitelist-ip whitelist IP for specific service\n", + " --whtlst -2 whitelist-ip whitelist IP for specific service\n" + " --quic itef quic protocol service\n", DEF_SCHED); exit(exit_status); @@ -2175,6 +2183,8 @@ print_service_entry(dpvs_service_compat_t *se, unsigned int format) printf(" pp%s", proxy_protocol_str(se->proxy_protocol)); if (se->flags & IP_VS_SVC_F_EXPIRE_QUIESCENT) printf(" expire-quiescent"); + if (se->flags & IP_VS_SVC_F_QUIC && se->proto == IPPROTO_UDP) + printf(" quic"); if (se->check_conf.types) { printf(" dest-check"); if (dest_check_passive(&se->check_conf)) { diff --git a/tools/keepalived/keepalived/check/check_data.c b/tools/keepalived/keepalived/check/check_data.c index ba3ece87e..c80b1a592 100644 --- a/tools/keepalived/keepalived/check/check_data.c +++ b/tools/keepalived/keepalived/check/check_data.c @@ -513,6 +513,7 @@ dump_vs(FILE *fp, const void *data) conf_write(fp, " SYN proxy is %s", vs->syn_proxy ? "ON" : "OFF"); conf_write(fp, " expire_quiescent_conn is %s", vs->expire_quiescent_conn ? "ON" : "OFF"); + conf_write(fp, " quic is %s", vs->quic ? "ON" : "OFF"); if (vs->hash_target) { switch (vs->hash_target) { @@ -604,6 +605,7 @@ alloc_vs(const char *param1, const char *param2) new->conn_timeout = 0; new->syn_proxy = false; new->expire_quiescent_conn = false; + new->quic = false; new->local_addr_gname = NULL; new->blklst_addr_gname = NULL; new->whtlst_addr_gname = NULL; diff --git a/tools/keepalived/keepalived/check/check_parser.c b/tools/keepalived/keepalived/check/check_parser.c index 8c4f26dcf..1511d6350 100644 --- a/tools/keepalived/keepalived/check/check_parser.c +++ b/tools/keepalived/keepalived/check/check_parser.c @@ -956,6 +956,13 @@ expire_quiescent_handler(const vector_t *strvec) vs->expire_quiescent_conn = true; } +static void +quic_handler(const vector_t *strvec) +{ + virtual_server_t *vs = LIST_TAIL_DATA(check_data->vs); + vs->quic = true; +} + static void bind_dev_handler(const vector_t *strvec) { @@ -1240,6 +1247,7 @@ init_check_keywords(bool active) install_keyword("waddr_group_name", &whtlst_gname_handler); install_keyword("syn_proxy", &syn_proxy_handler); install_keyword("expire_quiescent_conn", &expire_quiescent_handler); + install_keyword("quic", &quic_handler); install_keyword("vip_bind_dev", &bind_dev_handler); } diff --git a/tools/keepalived/keepalived/check/ipvswrapper.c b/tools/keepalived/keepalived/check/ipvswrapper.c index 61f590be9..d3cb4dbd4 100755 --- a/tools/keepalived/keepalived/check/ipvswrapper.c +++ b/tools/keepalived/keepalived/check/ipvswrapper.c @@ -795,6 +795,10 @@ static void ipvs_set_srule(int cmd, dpvs_service_compat_t *srule, virtual_server srule->flags |= IP_VS_SVC_F_EXPIRE_QUIESCENT; } + if (vs->quic) { + srule->flags |= IP_VS_SVC_F_QUIC; + } + if (!strcmp(vs->sched, "conhash")) { if (vs->hash_target) { if ((srule->proto != IPPROTO_UDP) && diff --git a/tools/keepalived/keepalived/check/ipwrapper.c b/tools/keepalived/keepalived/check/ipwrapper.c index 80980b0ba..7bc45d1c2 100755 --- a/tools/keepalived/keepalived/check/ipwrapper.c +++ b/tools/keepalived/keepalived/check/ipwrapper.c @@ -1400,6 +1400,7 @@ clear_diff_services(list old_checkers_queue) vs->bps != new_vs->bps || vs->limit_proportion != new_vs->limit_proportion || vs->hash_target != new_vs->hash_target || vs->syn_proxy != new_vs->syn_proxy || vs->expire_quiescent_conn != new_vs->expire_quiescent_conn || + vs->quic != new_vs->quic || strcmp(vs->srange, new_vs->srange) || strcmp(vs->drange, new_vs->drange) || strcmp(vs->iifname, new_vs->iifname) || strcmp(vs->oifname, new_vs->oifname)) { ipvs_cmd(IP_VS_SO_SET_EDIT, new_vs, NULL); diff --git a/tools/keepalived/keepalived/include/check_data.h b/tools/keepalived/keepalived/include/check_data.h index 2d5debc66..14650b644 100644 --- a/tools/keepalived/keepalived/include/check_data.h +++ b/tools/keepalived/keepalived/include/check_data.h @@ -236,6 +236,7 @@ typedef struct _virtual_server { * the service from IPVS topology. */ bool syn_proxy; bool expire_quiescent_conn; + bool quic; unsigned int connection_to; /* connection time-out */ unsigned long delay_loop; /* Interval between running checker */ unsigned long warmup; /* max random timeout to start checker */ @@ -314,6 +315,7 @@ static inline bool quorum_equal(const notify_script_t *quorum1, (X)->hash_target == (Y)->hash_target &&\ (X)->syn_proxy == (Y)->syn_proxy &&\ (X)->expire_quiescent_conn == (Y)->expire_quiescent_conn &&\ + (X)->quic == (Y)->quic &&\ quorum_equal((X)->notify_quorum_up, (Y)->notify_quorum_up) &&\ quorum_equal((X)->notify_quorum_down, (Y)->notify_quorum_down) &&\ !strcmp((X)->sched, (Y)->sched) &&\ From ef1cb820c6330ea283a3fa8a77ff3fdfcc97ad1d Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 24 May 2024 19:03:31 +0800 Subject: [PATCH 33/63] patch: nginx v1.26.0 patches for toa nat64, uoa, and quic connection migration Signed-off-by: ywc689 --- kmod/toa/example_nat64/nginx/README.md | 32 -- patch/nginx/README.md | 80 ++++ .../v1.14.0}/nginx-1.14.0-nat64-toa.patch | 0 ...patch-toa-for-nat64-and-uoa-for-quic.patch | 398 ++++++++++++++++++ ...c-connection-migration-for-l4lb-dpvs.patch | 248 +++++++++++ 5 files changed, 726 insertions(+), 32 deletions(-) delete mode 100644 kmod/toa/example_nat64/nginx/README.md create mode 100644 patch/nginx/README.md rename {kmod/toa/example_nat64/nginx => patch/nginx/v1.14.0}/nginx-1.14.0-nat64-toa.patch (100%) create mode 100644 patch/nginx/v1.26.0/0001-patch-toa-for-nat64-and-uoa-for-quic.patch create mode 100644 patch/nginx/v1.26.0/0002-patch-quic-connection-migration-for-l4lb-dpvs.patch diff --git a/kmod/toa/example_nat64/nginx/README.md b/kmod/toa/example_nat64/nginx/README.md deleted file mode 100644 index 3e3190ee9..000000000 --- a/kmod/toa/example_nat64/nginx/README.md +++ /dev/null @@ -1,32 +0,0 @@ -This patch is for Nginx to get real client ip by 'toa_remote_addr' -when you are using NAT64 mode(VIP is IPv6 while RS is IPv4). -You can use this patch only when toa module is installed. - -Here is an exampe to configure http block in nginx.conf: - -``` -http { - include mime.types; - default_type application/octet-stream; - - log_format main '$toa_remote_addr $toa_remote_port $remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /data/nginx/logs/access.log main; - - keepalive_timeout 65; - - server { - listen 80; - server_name localhost; - - access_log /data/nginx/logs/access.log main; - - location / { - proxy_set_header X-Forwarded-For $toa_remote_addr; - proxy_pass http://192.168.1.1; - } - } -} -``` diff --git a/patch/nginx/README.md b/patch/nginx/README.md new file mode 100644 index 000000000..2614a3f5a --- /dev/null +++ b/patch/nginx/README.md @@ -0,0 +1,80 @@ +Nginx Patches for DPVS +----- + +The directory is arranged to place nginx patch files for DPVS. More specifically, it contains the following patches. + +* TOA patch for originating client IP/port derived from DPVS NAT64 translation +* UOA patch for originating client IP/port derived from DPVS UDP FNAT/NAT64 translation in QUIC/HTTP3 +* QUIC Server Connection ID patch for connection migration + +## TOA NAT64 + +Nginx can get the originating client IP address and Port NAT64'ed by DPVS by utilizing nginx variables 'toa_remote_addr' and 'toa_remote_port' respectively. It works when and only when the TOA kernel module has already installed successfully on the nginx server. + +This is an exampe configuration of nginx with TOA patch for NAT64. + +``` +http { + include mime.types; + default_type application/octet-stream; + + log_format nat64 '$remote_addr $toa_remote_addr :$toa_remote_port - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for" ' + '$request_length $upstream_response_time $upstream_addr'; + + access_log logs/access.log nat64; + + # more other configs ...... + +} +``` + +## UOA QUIC/HTTP3 + +Nginx can get the originating client IP address and Port NAT'ed by DPVS by utilizing nginx variables 'uoa_remote_addr' and 'uoa_remote_port' respectively. Both IPv4-IPv4 and IPv6-IPv6 NAT and NAT64(IPv6-IPv4 NAT) as well are supported. It works when and only when the UOA kernel module has already installed sucessfully on the nginx server. + +This is an exampe configuration of nginx with UOA patch. + +``` +http { + include mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http3" ' + '"$http_x_forwarded_for" $request_length $upstream_response_time $upstream_addr'; + + log_format quic '$remote_addr $uoa_remote_addr :$uoa_remote_port - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http3" ' + '"$http_x_forwarded_for" $request_length $upstream_response_time $upstream_addr'; + + access_log logs/access.log main; + + # more other configs ...... + + + server { + listen 443 quic reuseport; + listen 443 ssl; + + server_name qlb-test.qiyi.domain; + + access_log logs/quic.access.log quic; + + ssl_certificate certs/cert.pem; + ssl_certificate_key certs/key.pem; + + location / { + add_header Alt-Svc 'h3=":2443"; ma=86400'; + root html; + index index.html index.htm; + } + } +} +``` + +## Quic Server Connection ID + +It requires changes to Quic Server Connection ID(SCID) both in DPVS and Nginx to support the feature of QUIC connection migration. DPVS depends on Server IP/Port information encoded in SCID to schedule a migrating connection to the right nginx server where the previous connection resides, and Nginx relies on the socket cookie compiled in SCID to make a migrating connection be processed on the same listening socket as the previous one. Note that eBPF (bpf_sk_select_reuseport) is used in Nginx for QUIC connection migration, which requires Linux 5.7+. + +The patch adds Nginx server address information into SCID, and fixes its collision problem with Nginx's socket cookie. The server address contains 24 least significant bits(LSB) for IPv4, and 32 LSB for IPv6, and compliant with DPVS DCID format specification defined in [ipvs/quic.h](../../include/ipvs/quic.h). The server port is not included in SCID. diff --git a/kmod/toa/example_nat64/nginx/nginx-1.14.0-nat64-toa.patch b/patch/nginx/v1.14.0/nginx-1.14.0-nat64-toa.patch similarity index 100% rename from kmod/toa/example_nat64/nginx/nginx-1.14.0-nat64-toa.patch rename to patch/nginx/v1.14.0/nginx-1.14.0-nat64-toa.patch diff --git a/patch/nginx/v1.26.0/0001-patch-toa-for-nat64-and-uoa-for-quic.patch b/patch/nginx/v1.26.0/0001-patch-toa-for-nat64-and-uoa-for-quic.patch new file mode 100644 index 000000000..8f9838523 --- /dev/null +++ b/patch/nginx/v1.26.0/0001-patch-toa-for-nat64-and-uoa-for-quic.patch @@ -0,0 +1,398 @@ +From b5137f7826d2fa85352cbb1afe5f2317a4f3abd1 Mon Sep 17 00:00:00 2001 +From: wencyu +Date: Fri, 24 May 2024 16:37:06 +0800 +Subject: [PATCH 1/7] patch: toa for nat64 and uoa for quic + +Signed-off-by: wencyu +--- + src/core/ngx_connection.h | 5 ++ + src/core/ngx_inet.h | 44 +++++++++++ + src/event/ngx_event_accept.c | 23 +++++- + src/event/quic/ngx_event_quic_streams.c | 15 +++- + src/event/quic/ngx_event_quic_udp.c | 38 ++++++++- + src/http/ngx_http_variables.c | 133 ++++++++++++++++++++++++++++++++ + 6 files changed, 255 insertions(+), 3 deletions(-) + +diff --git a/src/core/ngx_connection.h b/src/core/ngx_connection.h +index 84dd804..0ce4c69 100644 +--- a/src/core/ngx_connection.h ++++ b/src/core/ngx_connection.h +@@ -147,6 +147,11 @@ struct ngx_connection_s { + socklen_t socklen; + ngx_str_t addr_text; + ++ union { ++ struct sockaddr *toa_addr; ++ struct sockaddr *uoa_addr; ++ }; ++ + ngx_proxy_protocol_t *proxy_protocol; + + #if (NGX_QUIC || NGX_COMPAT) +diff --git a/src/core/ngx_inet.h b/src/core/ngx_inet.h +index 19050fc..3365c8e 100644 +--- a/src/core/ngx_inet.h ++++ b/src/core/ngx_inet.h +@@ -106,6 +106,50 @@ typedef struct { + } ngx_url_t; + + ++/* toa socket options, now only for nat64 */ ++enum { ++ TOA_BASE_CTL = 4096, ++ /* set */ ++ TOA_SO_SET_MAX = TOA_BASE_CTL, ++ /* get */ ++ TOA_SO_GET_LOOKUP = TOA_BASE_CTL, ++ TOA_SO_GET_MAX = TOA_SO_GET_LOOKUP, ++}; ++ ++/* uoa socket options */ ++enum { ++ UOA_BASE_CTL = 2048, ++ /* set */ ++ UOA_SO_SET_MAX = UOA_BASE_CTL, ++ /* get */ ++ UOA_SO_GET_LOOKUP = UOA_BASE_CTL, ++ UOA_SO_GET_MAX = UOA_SO_GET_LOOKUP, ++}; ++ ++typedef struct { ++ struct in6_addr saddr; ++ uint16_t port; ++} toa_nat64_peer_t; ++ ++typedef union { ++ struct in_addr in; ++ struct in6_addr in6; ++} uoa_addr_t; ++ ++typedef struct { ++ /* input */ ++ uint16_t af; ++ uoa_addr_t saddr; ++ uoa_addr_t daddr; ++ uint16_t sport; ++ uint16_t dport; ++ /* output */ ++ uint16_t real_af; ++ uoa_addr_t real_saddr; ++ uint16_t real_sport; ++} __attribute__((__packed__)) uoa_param_map_t; ++ ++ + in_addr_t ngx_inet_addr(u_char *text, size_t len); + #if (NGX_HAVE_INET6) + ngx_int_t ngx_inet6_addr(u_char *p, size_t len, u_char *addr); +diff --git a/src/event/ngx_event_accept.c b/src/event/ngx_event_accept.c +index 2703879..66f28b2 100644 +--- a/src/event/ngx_event_accept.c ++++ b/src/event/ngx_event_accept.c +@@ -20,7 +20,7 @@ static void ngx_close_accepted_connection(ngx_connection_t *c); + void + ngx_event_accept(ngx_event_t *ev) + { +- socklen_t socklen; ++ socklen_t socklen, toa64_addrlen; + ngx_err_t err; + ngx_log_t *log; + ngx_uint_t level; +@@ -30,6 +30,7 @@ ngx_event_accept(ngx_event_t *ev) + ngx_listening_t *ls; + ngx_connection_t *c, *lc; + ngx_event_conf_t *ecf; ++ toa_nat64_peer_t toa64_addr; + #if (NGX_HAVE_ACCEPT4) + static ngx_uint_t use_accept4 = 1; + #endif +@@ -174,6 +175,26 @@ ngx_event_accept(ngx_event_t *ev) + + ngx_memcpy(c->sockaddr, &sa, socklen); + ++ ++ /* get nat64 toa remote addr & port */ ++ toa64_addrlen = sizeof(toa_nat64_peer_t); ++ if (getsockopt(s, IPPROTO_IP, TOA_SO_GET_LOOKUP, &toa64_addr, ++ &toa64_addrlen) == NGX_OK) { ++ struct sockaddr_in6 *toa64_sa; ++ c->toa_addr = ngx_palloc(c->pool, sizeof(struct sockaddr_in6)); ++ if (c->toa_addr == NULL) { ++ ngx_close_accepted_connection(c); ++ return; ++ } ++ ngx_memzero(c->toa_addr, sizeof(struct sockaddr_in6)); ++ toa64_sa = (struct sockaddr_in6 *)c->toa_addr; ++ toa64_sa->sin6_family = AF_INET6; ++ toa64_sa->sin6_addr = toa64_addr.saddr; ++ toa64_sa->sin6_port = toa64_addr.port; ++ } else { ++ c->toa_addr = NULL; ++ } ++ + log = ngx_palloc(c->pool, sizeof(ngx_log_t)); + if (log == NULL) { + ngx_close_accepted_connection(c); +diff --git a/src/event/quic/ngx_event_quic_streams.c b/src/event/quic/ngx_event_quic_streams.c +index 178b805..ce11cff 100644 +--- a/src/event/quic/ngx_event_quic_streams.c ++++ b/src/event/quic/ngx_event_quic_streams.c +@@ -646,7 +646,7 @@ ngx_quic_create_stream(ngx_connection_t *c, uint64_t id) + ngx_pool_t *pool; + ngx_uint_t reusable; + ngx_queue_t *q; +- struct sockaddr *sockaddr; ++ struct sockaddr *sockaddr, *uoa_addr; + ngx_connection_t *sc; + ngx_quic_stream_t *qs; + ngx_pool_cleanup_t *cln; +@@ -707,6 +707,18 @@ ngx_quic_create_stream(ngx_connection_t *c, uint64_t id) + + ngx_memcpy(sockaddr, c->sockaddr, c->socklen); + ++ if (c->uoa_addr) { ++ uoa_addr = ngx_palloc(pool, sizeof(struct sockaddr_storage)); ++ if (uoa_addr == NULL) { ++ ngx_destroy_pool(pool); ++ ngx_queue_insert_tail(&qc->streams.free, &qs->queue); ++ return NULL; ++ } ++ ngx_memcpy(uoa_addr, c->uoa_addr, sizeof(struct sockaddr_storage)); ++ } else { ++ uoa_addr = NULL; ++ } ++ + if (c->addr_text.data) { + addr_text.data = ngx_pnalloc(pool, c->addr_text.len); + if (addr_text.data == NULL) { +@@ -743,6 +755,7 @@ ngx_quic_create_stream(ngx_connection_t *c, uint64_t id) + sc->ssl = c->ssl; + sc->sockaddr = sockaddr; + sc->socklen = c->socklen; ++ sc->uoa_addr = uoa_addr; + sc->listening = c->listening; + sc->addr_text = addr_text; + sc->local_sockaddr = c->local_sockaddr; +diff --git a/src/event/quic/ngx_event_quic_udp.c b/src/event/quic/ngx_event_quic_udp.c +index 15b54bc..e94e0f5 100644 +--- a/src/event/quic/ngx_event_quic_udp.c ++++ b/src/event/quic/ngx_event_quic_udp.c +@@ -24,7 +24,7 @@ ngx_quic_recvmsg(ngx_event_t *ev) + ngx_buf_t buf; + ngx_log_t *log; + ngx_err_t err; +- socklen_t socklen, local_socklen; ++ socklen_t socklen, local_socklen, uoamap_len; + ngx_event_t *rev, *wev; + struct iovec iov[1]; + struct msghdr msg; +@@ -34,6 +34,7 @@ ngx_quic_recvmsg(ngx_event_t *ev) + ngx_event_conf_t *ecf; + ngx_connection_t *c, *lc; + ngx_quic_socket_t *qsock; ++ uoa_param_map_t uoamap; + static u_char buffer[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE]; + + #if (NGX_HAVE_ADDRINFO_CMSG) +@@ -238,6 +239,41 @@ ngx_quic_recvmsg(ngx_event_t *ev) + + ngx_memcpy(c->sockaddr, sockaddr, socklen); + ++ /* parse uoa address */ ++ if (sockaddr->sa_family == AF_INET || sockaddr->sa_family == AF_INET6) { ++ uoamap_len = sizeof(uoamap); ++ ngx_memzero(&uoamap, uoamap_len); ++ uoamap.af = sockaddr->sa_family; ++ if (uoamap.af == AF_INET) { ++ uoamap.saddr.in = ((ngx_sockaddr_t *)sockaddr)->sockaddr_in.sin_addr; ++ uoamap.sport = ((ngx_sockaddr_t *)sockaddr)->sockaddr_in.sin_port; ++ uoamap.dport = ((ngx_sockaddr_t *)local_sockaddr)->sockaddr_in.sin_port; ++ } else { ++ uoamap.saddr.in6 = ((ngx_sockaddr_t *)sockaddr)->sockaddr_in6.sin6_addr; ++ uoamap.sport = ((ngx_sockaddr_t *)sockaddr)->sockaddr_in6.sin6_port; ++ uoamap.dport = ((ngx_sockaddr_t *)local_sockaddr)->sockaddr_in6.sin6_port; ++ } ++ if (getsockopt(lc->fd, IPPROTO_IP, UOA_SO_GET_LOOKUP, &uoamap, &uoamap_len) ++ == NGX_OK) { ++ c->uoa_addr = ngx_palloc(c->pool, sizeof(struct sockaddr_storage)); ++ if (c->uoa_addr == NULL) { ++ ngx_quic_close_accepted_connection(c); ++ return; ++ } ++ ngx_memzero(c->uoa_addr, sizeof(struct sockaddr_storage)); ++ c->uoa_addr->sa_family = uoamap.real_af; ++ if (uoamap.real_af == AF_INET) { ++ ((ngx_sockaddr_t *)c->uoa_addr)->sockaddr_in.sin_port = uoamap.real_sport; ++ ((ngx_sockaddr_t *)c->uoa_addr)->sockaddr_in.sin_addr = uoamap.real_saddr.in; ++ } else { ++ ((ngx_sockaddr_t *)c->uoa_addr)->sockaddr_in6.sin6_port = uoamap.real_sport; ++ ((ngx_sockaddr_t *)c->uoa_addr)->sockaddr_in6.sin6_addr = uoamap.real_saddr.in6; ++ } ++ } else { ++ c->uoa_addr = NULL; ++ } ++ } ++ + log = ngx_palloc(c->pool, sizeof(ngx_log_t)); + if (log == NULL) { + ngx_quic_close_accepted_connection(c); +diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c +index 4f0bd0e..55c6bd7 100644 +--- a/src/http/ngx_http_variables.c ++++ b/src/http/ngx_http_variables.c +@@ -57,6 +57,14 @@ static ngx_int_t ngx_http_variable_remote_addr(ngx_http_request_t *r, + ngx_http_variable_value_t *v, uintptr_t data); + static ngx_int_t ngx_http_variable_remote_port(ngx_http_request_t *r, + ngx_http_variable_value_t *v, uintptr_t data); ++static ngx_int_t ngx_http_variable_toa_remote_addr(ngx_http_request_t *r, ++ ngx_http_variable_value_t *v, uintptr_t data); ++static ngx_int_t ngx_http_variable_toa_remote_port(ngx_http_request_t *r, ++ ngx_http_variable_value_t *v, uintptr_t data); ++static ngx_int_t ngx_http_variable_uoa_remote_addr(ngx_http_request_t *r, ++ ngx_http_variable_value_t *v, uintptr_t data); ++static ngx_int_t ngx_http_variable_uoa_remote_port(ngx_http_request_t *r, ++ ngx_http_variable_value_t *v, uintptr_t data); + static ngx_int_t ngx_http_variable_proxy_protocol_addr(ngx_http_request_t *r, + ngx_http_variable_value_t *v, uintptr_t data); + static ngx_int_t ngx_http_variable_proxy_protocol_port(ngx_http_request_t *r, +@@ -200,6 +208,14 @@ static ngx_http_variable_t ngx_http_core_variables[] = { + + { ngx_string("remote_port"), NULL, ngx_http_variable_remote_port, 0, 0, 0 }, + ++ { ngx_string("toa_remote_addr"), NULL, ngx_http_variable_toa_remote_addr, 0, 0, 0 }, ++ ++ { ngx_string("toa_remote_port"), NULL, ngx_http_variable_toa_remote_port, 0, 0, 0 }, ++ ++ { ngx_string("uoa_remote_addr"), NULL, ngx_http_variable_uoa_remote_addr, 0, 0, 0 }, ++ ++ { ngx_string("uoa_remote_port"), NULL, ngx_http_variable_uoa_remote_port, 0, 0, 0 }, ++ + { ngx_string("proxy_protocol_addr"), NULL, + ngx_http_variable_proxy_protocol_addr, + offsetof(ngx_proxy_protocol_t, src_addr), 0, 0 }, +@@ -1335,6 +1351,123 @@ ngx_http_variable_remote_port(ngx_http_request_t *r, + return NGX_OK; + } + ++static ngx_int_t ngx_http_variable_toa_remote_addr(ngx_http_request_t *r, ++ ngx_http_variable_value_t *v, uintptr_t data) { ++ struct sockaddr_in6 *sa6; ++ size_t len; ++ ++ len = r->connection->toa_addr ? NGX_INET6_ADDRSTRLEN : 1; ++ v->data = ngx_pnalloc(r->pool, len); ++ if (v->data == NULL) { ++ return NGX_ERROR; ++ } ++ ++ v->len = 0; ++ v->valid = 1; ++ v->no_cacheable = 0; ++ v->not_found = 0; ++ ++ if (r->connection->toa_addr) { ++ sa6 = (struct sockaddr_in6 *)r->connection->toa_addr; ++ v->len = ngx_inet_ntop(sa6->sin6_family, &sa6->sin6_addr, (u_char *)v->data, len); ++ } else { ++ v->data[0] = '-'; ++ v->len = 1; ++ } ++ ++ return NGX_OK; ++} ++ ++static ngx_int_t ngx_http_variable_toa_remote_port(ngx_http_request_t *r, ++ ngx_http_variable_value_t *v, uintptr_t data) { ++ ngx_uint_t port; ++ size_t len; ++ ++ len = r->connection->toa_addr ? sizeof("65535") - 1 : 1; ++ v->data = ngx_pnalloc(r->pool, len); ++ if (v->data == NULL) { ++ return NGX_ERROR; ++ } ++ ++ v->len = 0; ++ v->valid = 1; ++ v->no_cacheable = 0; ++ v->not_found = 0; ++ ++ if (r->connection->toa_addr) { ++ port = ngx_inet_get_port(r->connection->toa_addr); ++ if (port > 0 && port < 65536) { ++ v->len = ngx_sprintf(v->data, "%ui", port) - v->data; ++ } ++ } else { ++ v->data[0] = '-'; ++ v->len = 1; ++ } ++ ++ return NGX_OK; ++} ++ ++static ngx_int_t ngx_http_variable_uoa_remote_addr(ngx_http_request_t *r, ++ ngx_http_variable_value_t *v, uintptr_t data) { ++ struct sockaddr_in *sa; ++ struct sockaddr_in6 *sa6; ++ size_t len; ++ ++ len = r->connection->uoa_addr ? NGX_INET6_ADDRSTRLEN : 1; ++ v->data = ngx_pnalloc(r->pool, len); ++ if (v->data == NULL) { ++ return NGX_ERROR; ++ } ++ ++ v->len = 0; ++ v->valid = 1; ++ v->no_cacheable = 0; ++ v->not_found = 0; ++ ++ if (r->connection->uoa_addr) { ++ if (r->connection->uoa_addr->sa_family == AF_INET6) { ++ sa6 = (struct sockaddr_in6 *)r->connection->uoa_addr; ++ v->len = ngx_inet_ntop(sa6->sin6_family, &sa6->sin6_addr, (u_char *)v->data, len); ++ } else { ++ sa = (struct sockaddr_in *)r->connection->uoa_addr; ++ v->len = ngx_inet_ntop(sa->sin_family, &sa->sin_addr, (u_char *)v->data, len); ++ } ++ } else { ++ v->data[0] = '-'; ++ v->len = 1; ++ } ++ ++ return NGX_OK; ++} ++ ++static ngx_int_t ngx_http_variable_uoa_remote_port(ngx_http_request_t *r, ++ ngx_http_variable_value_t *v, uintptr_t data) { ++ ngx_uint_t port; ++ size_t len; ++ ++ len = r->connection->uoa_addr ? sizeof("65535") - 1 : 1; ++ v->data = ngx_pnalloc(r->pool, len); ++ if (v->data == NULL) { ++ return NGX_ERROR; ++ } ++ ++ v->len = 0; ++ v->valid = 1; ++ v->no_cacheable = 0; ++ v->not_found = 0; ++ ++ if (r->connection->uoa_addr) { ++ port = ngx_inet_get_port(r->connection->uoa_addr); ++ if (port > 0 && port < 65536) { ++ v->len = ngx_sprintf(v->data, "%ui", port) - v->data; ++ } ++ } else { ++ v->data[0] = '-'; ++ v->len = 1; ++ } ++ ++ return NGX_OK; ++} + + static ngx_int_t + ngx_http_variable_proxy_protocol_addr(ngx_http_request_t *r, +-- +1.8.3.1 + diff --git a/patch/nginx/v1.26.0/0002-patch-quic-connection-migration-for-l4lb-dpvs.patch b/patch/nginx/v1.26.0/0002-patch-quic-connection-migration-for-l4lb-dpvs.patch new file mode 100644 index 000000000..52962a860 --- /dev/null +++ b/patch/nginx/v1.26.0/0002-patch-quic-connection-migration-for-l4lb-dpvs.patch @@ -0,0 +1,248 @@ +From 832740515032f6169635f13eacc0fa50d5560d51 Mon Sep 17 00:00:00 2001 +From: wencyu +Date: Wed, 29 May 2024 10:20:09 +0800 +Subject: [PATCH 2/7] patch: quic connection migration for l4lb/dpvs + +Signed-off-by: wencyu +--- + src/event/quic/bpf/ngx_quic_reuseport_helper.c | 8 ++-- + src/event/quic/ngx_event_quic_bpf_code.c | 58 +++++++++++++----------- + src/event/quic/ngx_event_quic_connid.c | 62 ++++++++++++++++++++++++++ + src/event/quic/ngx_event_quic_transport.c | 1 + + 4 files changed, 101 insertions(+), 28 deletions(-) + +diff --git a/src/event/quic/bpf/ngx_quic_reuseport_helper.c b/src/event/quic/bpf/ngx_quic_reuseport_helper.c +index 999e760..bdca492 100644 +--- a/src/event/quic/bpf/ngx_quic_reuseport_helper.c ++++ b/src/event/quic/bpf/ngx_quic_reuseport_helper.c +@@ -76,7 +76,7 @@ int ngx_quic_select_socket_by_dcid(struct sk_reuseport_md *ctx) + int rc; + __u64 key; + size_t len, offset; +- unsigned char *start, *end, *data, *dcid; ++ unsigned char *start, *end, *data, *dcid, *cookie; + + start = ctx->data; + end = (unsigned char *) ctx->data_end; +@@ -104,12 +104,14 @@ int ngx_quic_select_socket_by_dcid(struct sk_reuseport_md *ctx) + dcid = &data[1]; + advance_data(len); /* we expect the packet to have full DCID */ + ++ cookie = dcid + (len - sizeof(__u64)); /* socket cookie is at the tail of DCID */ ++ + /* make verifier happy */ +- if (dcid + sizeof(__u64) > end) { ++ if (cookie + sizeof(__u64) > end) { + goto failed; + } + +- key = ngx_quic_parse_uint64(dcid); ++ key = ngx_quic_parse_uint64(cookie); + + rc = bpf_sk_select_reuseport(ctx, &ngx_quic_sockmap, &key, 0); + +diff --git a/src/event/quic/ngx_event_quic_bpf_code.c b/src/event/quic/ngx_event_quic_bpf_code.c +index 5c9dea1..1124c04 100644 +--- a/src/event/quic/ngx_event_quic_bpf_code.c ++++ b/src/event/quic/ngx_event_quic_bpf_code.c +@@ -7,62 +7,69 @@ + + + static ngx_bpf_reloc_t bpf_reloc_prog_ngx_quic_reuseport_helper[] = { +- { "ngx_quic_sockmap", 55 }, ++ { "ngx_quic_sockmap", 62 }, + }; + + static struct bpf_insn bpf_insn_prog_ngx_quic_reuseport_helper[] = { + /* opcode dst src offset imm */ + { 0x79, BPF_REG_4, BPF_REG_1, (int16_t) 0, 0x0 }, + { 0x79, BPF_REG_3, BPF_REG_1, (int16_t) 8, 0x0 }, ++ { 0xbf, BPF_REG_6, BPF_REG_4, (int16_t) 0, 0x0 }, ++ { 0x7, BPF_REG_6, BPF_REG_0, (int16_t) 0, 0x8 }, ++ { 0x2d, BPF_REG_6, BPF_REG_3, (int16_t) 61, 0x0 }, + { 0xbf, BPF_REG_2, BPF_REG_4, (int16_t) 0, 0x0 }, +- { 0x7, BPF_REG_2, BPF_REG_0, (int16_t) 0, 0x8 }, +- { 0x2d, BPF_REG_2, BPF_REG_3, (int16_t) 54, 0x0 }, +- { 0xbf, BPF_REG_5, BPF_REG_4, (int16_t) 0, 0x0 }, +- { 0x7, BPF_REG_5, BPF_REG_0, (int16_t) 0, 0x9 }, +- { 0x2d, BPF_REG_5, BPF_REG_3, (int16_t) 51, 0x0 }, +- { 0xb7, BPF_REG_5, BPF_REG_0, (int16_t) 0, 0x14 }, ++ { 0x7, BPF_REG_2, BPF_REG_0, (int16_t) 0, 0x9 }, ++ { 0x2d, BPF_REG_2, BPF_REG_3, (int16_t) 58, 0x0 }, ++ { 0xb7, BPF_REG_2, BPF_REG_0, (int16_t) 0, 0x14 }, ++ { 0xb7, BPF_REG_5, BPF_REG_0, (int16_t) 0, 0x8 }, + { 0xb7, BPF_REG_0, BPF_REG_0, (int16_t) 0, 0x9 }, +- { 0x71, BPF_REG_6, BPF_REG_2, (int16_t) 0, 0x0 }, ++ { 0x71, BPF_REG_6, BPF_REG_6, (int16_t) 0, 0x0 }, + { 0x67, BPF_REG_6, BPF_REG_0, (int16_t) 0, 0x38 }, + { 0xc7, BPF_REG_6, BPF_REG_0, (int16_t) 0, 0x38 }, +- { 0x65, BPF_REG_6, BPF_REG_0, (int16_t) 10, 0xffffffff }, ++ { 0x65, BPF_REG_6, BPF_REG_0, (int16_t) 11, 0xffffffff }, + { 0xbf, BPF_REG_2, BPF_REG_4, (int16_t) 0, 0x0 }, + { 0x7, BPF_REG_2, BPF_REG_0, (int16_t) 0, 0xd }, +- { 0x2d, BPF_REG_2, BPF_REG_3, (int16_t) 42, 0x0 }, ++ { 0x2d, BPF_REG_2, BPF_REG_3, (int16_t) 48, 0x0 }, + { 0xbf, BPF_REG_5, BPF_REG_4, (int16_t) 0, 0x0 }, + { 0x7, BPF_REG_5, BPF_REG_0, (int16_t) 0, 0xe }, +- { 0x2d, BPF_REG_5, BPF_REG_3, (int16_t) 39, 0x0 }, ++ { 0x2d, BPF_REG_5, BPF_REG_3, (int16_t) 45, 0x0 }, ++ { 0xb7, BPF_REG_5, BPF_REG_0, (int16_t) 0, 0xd }, + { 0xb7, BPF_REG_0, BPF_REG_0, (int16_t) 0, 0xe }, +- { 0x71, BPF_REG_5, BPF_REG_2, (int16_t) 0, 0x0 }, ++ { 0x71, BPF_REG_2, BPF_REG_2, (int16_t) 0, 0x0 }, + { 0xb7, BPF_REG_6, BPF_REG_0, (int16_t) 0, 0x8 }, +- { 0x2d, BPF_REG_6, BPF_REG_5, (int16_t) 35, 0x0 }, +- { 0xf, BPF_REG_5, BPF_REG_0, (int16_t) 0, 0x0 }, ++ { 0x2d, BPF_REG_6, BPF_REG_2, (int16_t) 40, 0x0 }, ++ { 0xbf, BPF_REG_6, BPF_REG_2, (int16_t) 0, 0x0 }, ++ { 0xf, BPF_REG_6, BPF_REG_0, (int16_t) 0, 0x0 }, ++ { 0xbf, BPF_REG_0, BPF_REG_4, (int16_t) 0, 0x0 }, ++ { 0xf, BPF_REG_0, BPF_REG_6, (int16_t) 0, 0x0 }, ++ { 0x2d, BPF_REG_0, BPF_REG_3, (int16_t) 35, 0x0 }, + { 0xf, BPF_REG_4, BPF_REG_5, (int16_t) 0, 0x0 }, +- { 0x2d, BPF_REG_4, BPF_REG_3, (int16_t) 32, 0x0 }, ++ { 0xf, BPF_REG_2, BPF_REG_4, (int16_t) 0, 0x0 }, + { 0xbf, BPF_REG_4, BPF_REG_2, (int16_t) 0, 0x0 }, +- { 0x7, BPF_REG_4, BPF_REG_0, (int16_t) 0, 0x9 }, +- { 0x2d, BPF_REG_4, BPF_REG_3, (int16_t) 29, 0x0 }, +- { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 1, 0x0 }, ++ { 0x7, BPF_REG_4, BPF_REG_0, (int16_t) 0, 0x1 }, ++ { 0x2d, BPF_REG_4, BPF_REG_3, (int16_t) 30, 0x0 }, ++ { 0x7, BPF_REG_2, BPF_REG_0, (int16_t) 0, 0xfffffff9 }, ++ { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 0, 0x0 }, + { 0x67, BPF_REG_4, BPF_REG_0, (int16_t) 0, 0x38 }, +- { 0x71, BPF_REG_3, BPF_REG_2, (int16_t) 2, 0x0 }, ++ { 0x71, BPF_REG_3, BPF_REG_2, (int16_t) 1, 0x0 }, + { 0x67, BPF_REG_3, BPF_REG_0, (int16_t) 0, 0x30 }, + { 0x4f, BPF_REG_3, BPF_REG_4, (int16_t) 0, 0x0 }, +- { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 3, 0x0 }, ++ { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 2, 0x0 }, + { 0x67, BPF_REG_4, BPF_REG_0, (int16_t) 0, 0x28 }, + { 0x4f, BPF_REG_3, BPF_REG_4, (int16_t) 0, 0x0 }, +- { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 4, 0x0 }, ++ { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 3, 0x0 }, + { 0x67, BPF_REG_4, BPF_REG_0, (int16_t) 0, 0x20 }, + { 0x4f, BPF_REG_3, BPF_REG_4, (int16_t) 0, 0x0 }, +- { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 5, 0x0 }, ++ { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 4, 0x0 }, + { 0x67, BPF_REG_4, BPF_REG_0, (int16_t) 0, 0x18 }, + { 0x4f, BPF_REG_3, BPF_REG_4, (int16_t) 0, 0x0 }, +- { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 6, 0x0 }, ++ { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 5, 0x0 }, + { 0x67, BPF_REG_4, BPF_REG_0, (int16_t) 0, 0x10 }, + { 0x4f, BPF_REG_3, BPF_REG_4, (int16_t) 0, 0x0 }, +- { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 7, 0x0 }, ++ { 0x71, BPF_REG_4, BPF_REG_2, (int16_t) 6, 0x0 }, + { 0x67, BPF_REG_4, BPF_REG_0, (int16_t) 0, 0x8 }, + { 0x4f, BPF_REG_3, BPF_REG_4, (int16_t) 0, 0x0 }, +- { 0x71, BPF_REG_2, BPF_REG_2, (int16_t) 8, 0x0 }, ++ { 0x71, BPF_REG_2, BPF_REG_2, (int16_t) 7, 0x0 }, + { 0x4f, BPF_REG_3, BPF_REG_2, (int16_t) 0, 0x0 }, + { 0x7b, BPF_REG_10, BPF_REG_3, (int16_t) 65528, 0x0 }, + { 0xbf, BPF_REG_3, BPF_REG_10, (int16_t) 0, 0x0 }, +@@ -86,3 +93,4 @@ ngx_bpf_program_t ngx_quic_reuseport_helper = { + .license = "BSD", + .type = BPF_PROG_TYPE_SK_REUSEPORT, + }; ++ +diff --git a/src/event/quic/ngx_event_quic_connid.c b/src/event/quic/ngx_event_quic_connid.c +index f508682..9046db1 100644 +--- a/src/event/quic/ngx_event_quic_connid.c ++++ b/src/event/quic/ngx_event_quic_connid.c +@@ -15,6 +15,8 @@ + #if (NGX_QUIC_BPF) + static ngx_int_t ngx_quic_bpf_attach_id(ngx_connection_t *c, u_char *id); + #endif ++static ngx_int_t ngx_quic_dcid_encode_server_info(ngx_connection_t *c, ++ u_char *id); + static ngx_int_t ngx_quic_retire_client_id(ngx_connection_t *c, + ngx_quic_client_id_t *cid); + static ngx_quic_client_id_t *ngx_quic_alloc_client_id(ngx_connection_t *c, +@@ -38,6 +40,12 @@ ngx_quic_create_server_id(ngx_connection_t *c, u_char *id) + } + #endif + ++ /* encode server info into DCID for L4LB/DPVS */ ++ if (ngx_quic_dcid_encode_server_info(c, id) != NGX_OK) { ++ ngx_log_error(NGX_LOG_ERR, c->log, 0, ++ "quic server info failed to be encoded"); ++ } ++ + return NGX_OK; + } + +@@ -69,6 +77,60 @@ ngx_quic_bpf_attach_id(ngx_connection_t *c, u_char *id) + + #endif + ++/* ++ * L4LB/DPVS QUIC Connction ID Format { ++ * First Octet (8), ++ * L3 Address Length (3), ++ * L4 Address Flag (1), ++ * L3 Address (8...64), ++ * [ L4 Address (16) ] ++ * Nonce (32...140) ++ * } ++ * ++ * Specifically for this case: ++ * L3 Address Length := 3 (IPv4), 4 (IPv6) ++ * L4 Address Flag := 0 ++ */ ++static ngx_int_t ++ngx_quic_dcid_encode_server_info(ngx_connection_t *c, u_char *id) ++{ ++ unsigned int len; ++ u_char *addr, *ptr; ++ struct sockaddr *sa; ++ ++ sa = c->local_sockaddr; ++ if (sa->sa_family == AF_INET) { ++ addr = (u_char *)(&((struct sockaddr_in *)sa)->sin_addr); ++ len = 3; ++ addr += (4 - len); ++ } else if (sa->sa_family == AF_INET6) { ++ addr = (u_char *)(&((struct sockaddr_in6 *)sa)->sin6_addr); ++ len = 4; ++ addr += (16 - len); ++ } else { ++ return NGX_OK; ++ } ++ ++ if (len + sizeof(uint64_t) + 2 > NGX_QUIC_SERVER_CID_LEN) { ++ return NGX_ERROR; ++ } ++ ++ ptr = id; ++ ptr++; ++ ++ *ptr = 0; ++ *ptr++ = (((len - 1) & 0x7) << 5) | ((*addr >> 4) & 0xf); ++ ++ while (--len > 0) { ++ *ptr++ = ((*addr & 0xf) << 4) | ((*(addr+1) >> 4) & 0xf); ++ addr++; ++ } ++ ++ *ptr &= 0xf; ++ *ptr |= ((*addr & 0xf) << 4); ++ ++ return NGX_OK; ++} + + ngx_int_t + ngx_quic_handle_new_connection_id_frame(ngx_connection_t *c, +diff --git a/src/event/quic/ngx_event_quic_transport.c b/src/event/quic/ngx_event_quic_transport.c +index 19670a6..1cbec6c 100644 +--- a/src/event/quic/ngx_event_quic_transport.c ++++ b/src/event/quic/ngx_event_quic_transport.c +@@ -2198,5 +2198,6 @@ ngx_quic_create_close(u_char *p, ngx_quic_frame_t *f) + void + ngx_quic_dcid_encode_key(u_char *dcid, uint64_t key) + { ++ dcid += (NGX_QUIC_SERVER_CID_LEN - sizeof(key)); + (void) ngx_quic_write_uint64(dcid, key); + } +-- +1.8.3.1 + From e5d466ed46149b79df9b44d0b119b29ffd5bc0f6 Mon Sep 17 00:00:00 2001 From: liwang03 Date: Wed, 5 Jun 2024 17:52:21 +0800 Subject: [PATCH 34/63] tools/ipvsadm: fix could not remove lip --- tools/keepalived/keepalived/check/libipvs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/keepalived/keepalived/check/libipvs.c b/tools/keepalived/keepalived/check/libipvs.c index f81efce7a..849ac51ad 100644 --- a/tools/keepalived/keepalived/check/libipvs.c +++ b/tools/keepalived/keepalived/check/libipvs.c @@ -293,7 +293,7 @@ int dpvs_del_laddr(dpvs_service_compat_t *svc, dpvs_laddr_table_t *laddr) dpvs_fill_ipaddr_conf(0, 0, laddr, ¶m); dpvs_setsockopt(SOCKOPT_SET_IFADDR_DEL, ¶m, sizeof(struct inet_addr_param)); - return dpvs_setsockopt(SOCKOPT_SET_LADDR_DEL, laddr, sizeof(laddr)); + return dpvs_setsockopt(SOCKOPT_SET_LADDR_DEL, laddr, sizeof(dpvs_laddr_table_t)); } /*for black list*/ From eff346a4f4b86438494bb758a241d8e4a3f58def Mon Sep 17 00:00:00 2001 From: Peng Yong Date: Thu, 20 Jun 2024 17:38:33 +0800 Subject: [PATCH 35/63] fix compile error on RHEL 9.4 --- kmod/uoa/uoa.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/kmod/uoa/uoa.c b/kmod/uoa/uoa.c index 28d487988..5e61b3c00 100644 --- a/kmod/uoa/uoa.c +++ b/kmod/uoa/uoa.c @@ -199,7 +199,13 @@ static int uoa_stats_percpu_show(struct seq_file *seq, void *arg) unsigned int start; do { + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,3,0) || \ + ( defined(RHEL_MAJOR) && ((RHEL_MAJOR == 9 && RHEL_MINOR > 3) || RHEL_MAJOR > 9)) + start = u64_stats_fetch_begin(&s->syncp); +#else start = u64_stats_fetch_begin_irq(&s->syncp); +#endif #endif success = s->success; miss = s->miss; @@ -209,7 +215,12 @@ static int uoa_stats_percpu_show(struct seq_file *seq, void *arg) saved = s->uoa_saved; ack_fail = s->uoa_ack_fail; #if LINUX_VERSION_CODE >= KERNEL_VERSION(4,1,0) +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,3,0) || \ + ( defined(RHEL_MAJOR) && ((RHEL_MAJOR == 9 && RHEL_MINOR > 3) || RHEL_MAJOR > 9)) + } while (u64_stats_fetch_retry(&s->syncp, start)); +#else } while (u64_stats_fetch_retry_irq(&s->syncp, start)); +#endif #endif seq_printf(seq, From b9fd58427a1bf3026bd6e793ecd631f6939a9bea Mon Sep 17 00:00:00 2001 From: liningjie Date: Wed, 10 Jul 2024 10:06:20 +0800 Subject: [PATCH 36/63] README: add ipvsadm RS port --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a7c1fa7c..138aa62a9 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ RS=192.168.100.2 ./dpip addr add ${VIP}/24 dev dpdk0 ./ipvsadm -A -t ${VIP}:80 -s rr -./ipvsadm -a -t ${VIP}:80 -r ${RS} -b +./ipvsadm -a -t ${VIP}:80 -r ${RS}:80 -b ./ipvsadm --add-laddr -z ${LIP} -t ${VIP}:80 -F dpdk0 $ From 8c49f436f75426159b3860dc002ecd2de3ca2d20 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Wed, 10 Jul 2024 12:51:00 +0800 Subject: [PATCH 37/63] ci: fix unsupported Node20 problem This is a temporary fix, and hopefully can work until Spring 2025. https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ Signed-off-by: ywc689 --- .github/workflows/build.yaml | 14 +++++++++----- .github/workflows/run.yaml | 10 ++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 360635bb4..a75508602 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -17,18 +17,22 @@ jobs: runs-on: self-hosted env: PKG_CONFIG_PATH: /data/dpdk/dpdklib/lib64/pkgconfig + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true steps: - - uses: actions/checkout@v2 - - name: make + - name: Checkout Code + uses: actions/checkout@v3 + - name: build run: make -j build-all: runs-on: self-hosted env: PKG_CONFIG_PATH: /data/dpdk/dpdklib/lib64/pkgconfig + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true steps: - - uses: actions/checkout@v2 - - name: config + - name: Checkout Code + uses: actions/checkout@v3 + - name: Config run: sed -i 's/=n$/=y/' config.mk - - name: make + - name: build run: make -j diff --git a/.github/workflows/run.yaml b/.github/workflows/run.yaml index 29b1a5423..f71248963 100644 --- a/.github/workflows/run.yaml +++ b/.github/workflows/run.yaml @@ -17,11 +17,13 @@ jobs: runs-on: self-hosted env: PKG_CONFIG_PATH: /data/dpdk/dpdklib/lib64/pkgconfig + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true steps: - - uses: actions/checkout@v2 - - name: make + - name: Checkout Code + uses: actions/checkout@v3 + - name: Build run: make -j - - name: install + - name: Install run: make install - - name: run-dpvs + - name: Run DPVS run: sudo dpvsci $(pwd)/bin/dpvs From 5c79c4fbfb0064ce6ac5601e836b5c97e7c52afb Mon Sep 17 00:00:00 2001 From: ywc689 Date: Mon, 8 Jul 2024 18:42:27 +0800 Subject: [PATCH 38/63] netif_addr: fix hw multicast address sync problems It fixes following problems in Previous implementation. 1. Neglected the fact that multicast IPv4/IPv6 address can be mapped to one multicast hw address, and a lower dpvs port may have multiple upper ports (such as vlan). 2. Multicast hw addresses could sync from kni multiple times or be deleted by mistake. 3. Interferences of linked down kni devices. Signed-off-by: ywc689 --- include/inetaddr.h | 4 +- include/netif.h | 28 +------- include/netif_addr.h | 51 ++++++++++--- src/inetaddr.c | 57 ++++++++++----- src/kni.c | 16 +++-- src/netif.c | 37 +++------- src/netif_addr.c | 166 +++++++++++++++++++++++-------------------- 7 files changed, 193 insertions(+), 166 deletions(-) diff --git a/include/inetaddr.h b/include/inetaddr.h index 9fb56269e..2a85e7611 100644 --- a/include/inetaddr.h +++ b/include/inetaddr.h @@ -46,7 +46,7 @@ struct inet_ifmcaddr { int af; union inet_addr addr; uint32_t flags; /* not used yet */ - rte_atomic32_t refcnt; + uint32_t refcnt; }; /* @@ -117,7 +117,7 @@ bool inet_chk_mcast_addr(int af, struct netif_port *dev, void inet_ifaddr_dad_failure(struct inet_ifaddr *ifa); -int idev_add_mcast_init(void *args); +int idev_add_mcast_init(struct netif_port *dev); int inet_addr_init(void); int inet_addr_term(void); diff --git a/include/netif.h b/include/netif.h index 951f50bb7..4347eb999 100644 --- a/include/netif.h +++ b/include/netif.h @@ -22,6 +22,7 @@ #include "list.h" #include "dpdk.h" #include "inetaddr.h" +#include "netif_addr.h" #include "global_data.h" #include "timer.h" #include "tc/tc.h" @@ -205,31 +206,6 @@ struct netif_ops { int (*op_get_xstats)(struct netif_port *dev, netif_nic_xstats_get_t **xstats); }; -struct netif_hw_addr { - struct list_head list; - struct rte_ether_addr addr; - rte_atomic32_t refcnt; - /* - * - sync only once! - * - * for HA in upper dev, no matter how many times it's added, - * only sync once to lower (when sync_cnt is zero). - * - * and HA (upper)'s refcnt++, to mark lower dev own's it. - * - * - when to unsync? - * - * when del if HA (upper dev)'s refcnt is 1 and syn_cnt is not zero. - * means lower dev is the only owner and need be unsync. - */ - int sync_cnt; -}; - -struct netif_hw_addr_list { - struct list_head addrs; - int count; -}; - struct netif_port { char name[IFNAMSIZ]; /* device name */ portid_t id; /* device id */ @@ -296,8 +272,6 @@ int netif_port_conf_get(struct netif_port *port, struct rte_eth_conf *eth_conf); int netif_port_conf_set(struct netif_port *port, const struct rte_eth_conf *conf); int netif_port_start(struct netif_port *port); // start nic and wait until up int netif_port_stop(struct netif_port *port); // stop nic -int netif_set_mc_list(struct netif_port *port); -int __netif_set_mc_list(struct netif_port *port); int netif_get_queue(struct netif_port *port, lcoreid_t id, queueid_t *qid); int netif_get_link(struct netif_port *dev, struct rte_eth_link *link); int netif_get_promisc(struct netif_port *dev, bool *promisc); diff --git a/include/netif_addr.h b/include/netif_addr.h index 1a6b97d71..c90da57cc 100644 --- a/include/netif_addr.h +++ b/include/netif_addr.h @@ -23,18 +23,42 @@ */ #ifndef __DPVS_NETIF_ADDR_H__ #define __DPVS_NETIF_ADDR_H__ -#include "netif.h" -int __netif_mc_add(struct netif_port *dev, const struct rte_ether_addr *addr); -int __netif_mc_del(struct netif_port *dev, const struct rte_ether_addr *addr); +enum { + HW_ADDR_F_FROM_KNI = 1, // from linux kni device in local layer +}; + +struct netif_hw_addr { + struct list_head list; + struct rte_ether_addr addr; + rte_atomic32_t refcnt; + uint16_t flags; + uint16_t sync_cnt; +}; + +struct netif_hw_addr_list { + struct list_head addrs; + int count; +}; + +struct netif_port; + +int __netif_hw_addr_add(struct netif_hw_addr_list *list, + const struct rte_ether_addr *addr, uint16_t flags); +int __netif_hw_addr_del(struct netif_hw_addr_list *list, + const struct rte_ether_addr *addr, uint16_t flags); + +int netif_set_mc_list(struct netif_port *dev); +int __netif_set_mc_list(struct netif_port *dev); + int netif_mc_add(struct netif_port *dev, const struct rte_ether_addr *addr); int netif_mc_del(struct netif_port *dev, const struct rte_ether_addr *addr); void netif_mc_flush(struct netif_port *dev); void netif_mc_init(struct netif_port *dev); -int __netif_mc_dump(struct netif_port *dev, - struct rte_ether_addr *addrs, size_t *naddr); -int netif_mc_dump(struct netif_port *dev, - struct rte_ether_addr *addrs, size_t *naddr); +int __netif_mc_dump(struct netif_port *dev, uint16_t filter_flags, + struct rte_ether_addr *addrs, size_t *naddr); +int netif_mc_dump(struct netif_port *dev, uint16_t filter_flags, + struct rte_ether_addr *addrs, size_t *naddr); int __netif_mc_print(struct netif_port *dev, char *buf, int *len, int *pnaddr); int netif_mc_print(struct netif_port *dev, @@ -45,10 +69,10 @@ int netif_mc_sync(struct netif_port *to, struct netif_port *from); int __netif_mc_unsync(struct netif_port *to, struct netif_port *from); int netif_mc_unsync(struct netif_port *to, struct netif_port *from); -int __netif_mc_sync_multiple(struct netif_port *to, struct netif_port *from); -int netif_mc_sync_multiple(struct netif_port *to, struct netif_port *from); -int __netif_mc_unsync_multiple(struct netif_port *to, struct netif_port *from); -int netif_mc_unsync_multiple(struct netif_port *to, struct netif_port *from); +int __netif_mc_sync_multiple(struct netif_port *to, struct netif_port *from, int sync_cnt); +int netif_mc_sync_multiple(struct netif_port *to, struct netif_port *from, int sync_cnt); +int __netif_mc_unsync_multiple(struct netif_port *to, struct netif_port *from, int sync_cnt); +int netif_mc_unsync_multiple(struct netif_port *to, struct netif_port *from, int sync_cnt); static inline int eth_addr_equal(const struct rte_ether_addr *addr1, const struct rte_ether_addr *addr2) @@ -69,4 +93,9 @@ static inline char *eth_addr_dump(const struct rte_ether_addr *ea, return buf; } +static bool inline hw_addr_from_kni(const struct netif_hw_addr *hwa) +{ + return !!(hwa->flags & HW_ADDR_F_FROM_KNI); +} + #endif /* __DPVS_NETIF_ADDR_H__ */ diff --git a/src/inetaddr.c b/src/inetaddr.c index d878ce06f..c5f13e0b6 100644 --- a/src/inetaddr.c +++ b/src/inetaddr.c @@ -90,15 +90,15 @@ static inline void idev_put(struct inet_device *idev) static inline void imc_hash(struct inet_ifmcaddr *imc, struct inet_device *idev) { list_add(&imc->d_list, &idev->this_ifm_list); - rte_atomic32_inc(&imc->refcnt); + ++imc->refcnt; } static inline void imc_unhash(struct inet_ifmcaddr *imc) { - assert(rte_atomic32_read(&imc->refcnt) > 1); + assert(imc->refcnt> 1); list_del(&imc->d_list); - rte_atomic32_dec(&imc->refcnt); + --imc->refcnt; } static struct inet_ifmcaddr *imc_lookup(int af, const struct inet_device *idev, @@ -109,7 +109,7 @@ static struct inet_ifmcaddr *imc_lookup(int af, const struct inet_device *idev, list_for_each_entry(imc, &idev->ifm_list[cid], d_list) { if (inet_addr_equal(af, &imc->addr, maddr)) { - rte_atomic32_inc(&imc->refcnt); + ++imc->refcnt; return imc; } } @@ -121,7 +121,7 @@ static void imc_put(struct inet_ifmcaddr *imc) { char ipstr[64]; - if (rte_atomic32_dec_and_test(&imc->refcnt)) { + if (--imc->refcnt == 0) { RTE_LOG(DEBUG, IFA, "[%02d] %s: del mcaddr %s\n", rte_lcore_id(), __func__, inet_ntop(imc->af, &imc->addr, ipstr, sizeof(ipstr))); @@ -136,20 +136,27 @@ static int idev_mc_add(int af, struct inet_device *idev, struct inet_ifmcaddr *imc; char ipstr[64]; - imc = imc_lookup(af, idev, maddr); - if (imc) { - imc_put(imc); - return EDPVS_EXIST; + if (imc_lookup(af, idev, maddr)) { + /* + * Hold the imc and return. + * + * Multiple IPv6 unicast address may be mapped to one IPv6 solicated-node + * multicast address. So increase the imc refcnt each time idev_mc_add called. + * + * Possibly imc added repeated? No, at least for now. The imc is set within the + * rigid program, not allowing user to configure it. + * */ + return EDPVS_OK; } imc = rte_calloc(NULL, 1, sizeof(struct inet_ifmcaddr), RTE_CACHE_LINE_SIZE); if (!imc) return EDPVS_NOMEM; - imc->af = af; - imc->idev = idev; - imc->addr = *maddr; - rte_atomic32_init(&imc->refcnt); + imc->af = af; + imc->idev = idev; + imc->addr = *maddr; + imc->refcnt = 1; imc_hash(imc, idev); @@ -169,7 +176,10 @@ static int idev_mc_del(int af, struct inet_device *idev, if (!imc) return EDPVS_NOTEXIST; - imc_unhash(imc); + if (--imc->refcnt == 2) { + imc_unhash(imc); + } + imc_put(imc); return EDPVS_OK; @@ -192,8 +202,6 @@ static int ifa_add_del_mcast(struct inet_ifaddr *ifa, bool add, bool is_master) if (add) { err = idev_mc_add(ifa->af, ifa->idev, &iaddr); - if (EDPVS_EXIST == err) - return EDPVS_OK; if (err) return err; if (is_master) { @@ -222,7 +230,7 @@ static int ifa_add_del_mcast(struct inet_ifaddr *ifa, bool add, bool is_master) } /* add ipv6 multicast address after port start */ -int idev_add_mcast_init(void *args) +static int __idev_add_mcast_init(void *args) { int err; struct inet_device *idev; @@ -278,6 +286,21 @@ int idev_add_mcast_init(void *args) return err; } +int idev_add_mcast_init(struct netif_port *dev) +{ + int err; + lcoreid_t cid; + + rte_eal_mp_remote_launch(__idev_add_mcast_init, dev, CALL_MAIN); + RTE_LCORE_FOREACH_WORKER(cid) { + err = rte_eal_wait_lcore(cid); + if (unlikely(err < 0)) + return err; + } + + return EDPVS_OK; +} + /* refer to linux:ipv6_chk_mcast_addr */ bool inet_chk_mcast_addr(int af, struct netif_port *dev, const union inet_addr *group, diff --git a/src/kni.c b/src/kni.c index 6da4799e5..b6af78ceb 100644 --- a/src/kni.c +++ b/src/kni.c @@ -108,7 +108,7 @@ static int kni_mc_list_cmp_set(struct netif_port *dev, rte_rwlock_write_lock(&dev->dev_lock); naddr_old = NELEMS(addrs_old); - err = __netif_mc_dump(dev, addrs_old, &naddr_old); + err = __netif_mc_dump(dev, HW_ADDR_F_FROM_KNI, addrs_old, &naddr_old); if (err != EDPVS_OK) { RTE_LOG(ERR, Kni, "%s: fail to get current mc list\n", __func__); goto out; @@ -162,14 +162,14 @@ static int kni_mc_list_cmp_set(struct netif_port *dev, /* nothing */ break; case 1: - err = __netif_mc_add(dev, &chg_lst.addrs[i]); + err = __netif_hw_addr_add(&dev->mc, &chg_lst.addrs[i], HW_ADDR_F_FROM_KNI); RTE_LOG(INFO, Kni, "%s: add mc addr: %s %s %s\n", __func__, eth_addr_dump(&chg_lst.addrs[i], mac, sizeof(mac)), dev->name, dpvs_strerror(err)); break; case 2: - err = __netif_mc_del(dev, &chg_lst.addrs[i]); + err = __netif_hw_addr_del(&dev->mc, &chg_lst.addrs[i], HW_ADDR_F_FROM_KNI); RTE_LOG(INFO, Kni, "%s: del mc addr: %s %s %s\n", __func__, eth_addr_dump(&chg_lst.addrs[i], mac, sizeof(mac)), @@ -246,7 +246,7 @@ static int kni_rtnl_check(void *arg) { struct netif_port *dev = arg; int fd = dev->kni.kni_rtnl_fd; - int n, i; + int n, i, link_flags = 0; char buf[4096]; struct nlmsghdr *nlh = (struct nlmsghdr *)buf; bool update = false; @@ -284,6 +284,14 @@ static int kni_rtnl_check(void *arg) /* note we should not update kni mac list for every event ! */ if (update) { RTE_LOG(DEBUG, Kni, "%d events received!\n", i); + if (EDPVS_OK != linux_get_link_status(dev->kni.name, &link_flags, NULL, 0)) { + RTE_LOG(ERR, Kni, "%s:undetermined kni link status\n", dev->kni.name); + return DTIMER_OK; + } + if (!(link_flags & IFF_UP)) { + RTE_LOG(DEBUG, Kni, "skip link down kni device %s\n", dev->kni.name); + return DTIMER_OK; + } if (kni_update_maddr(dev) == EDPVS_OK) RTE_LOG(DEBUG, Kni, "update maddr of %s OK!\n", dev->name); else diff --git a/src/netif.c b/src/netif.c index d2ae6cf91..4bc01a1bb 100644 --- a/src/netif.c +++ b/src/netif.c @@ -3298,7 +3298,7 @@ static int bond_set_mc_list(struct netif_port *dev) slave = dev->bond->master.slaves[i]; rte_rwlock_write_lock(&slave->dev_lock); - err = __netif_mc_sync_multiple(slave, dev); + err = __netif_mc_sync_multiple(slave, dev, dev->bond->master.slave_nb); rte_rwlock_write_unlock(&slave->dev_lock); if (err != EDPVS_OK) { @@ -3320,10 +3320,11 @@ static int dpdk_set_mc_list(struct netif_port *dev) if (rte_eth_allmulticast_get(dev->id) == 1) return EDPVS_OK; - err = __netif_mc_dump(dev, addrs, &naddr); + err = __netif_mc_dump(dev, 0, addrs, &naddr); if (err != EDPVS_OK) return err; + RTE_LOG(DEBUG, NETIF, "%s: configuring %lu multicast hw-addrs\n", dev->name, naddr); err = rte_eth_dev_set_mc_addr_list(dev->id, addrs, naddr); if (err) { RTE_LOG(WARNING, NETIF, "%s: rte_eth_dev_set_mc_addr_list failed -- %s," @@ -3506,6 +3507,7 @@ static struct netif_port* netif_rte_port_alloc(portid_t id, int nrxq, return NULL; } port->in_ptr->dev = port; + for (ii = 0; ii < DPVS_MAX_LCORE; ii++) { INIT_LIST_HEAD(&port->in_ptr->ifa_list[ii]); INIT_LIST_HEAD(&port->in_ptr->ifm_list[ii]); @@ -3916,7 +3918,6 @@ static int config_fdir_conf(struct rte_fdir_conf *fdir_conf) int netif_port_start(struct netif_port *port) { int ii, ret; - lcoreid_t cid; queueid_t qid; char promisc_on, allmulticast; char buf[512]; @@ -4058,13 +4059,10 @@ int netif_port_start(struct netif_port *port) port->netif_ops->op_update_addr(port); /* add in6_addr multicast address */ - rte_eal_mp_remote_launch(idev_add_mcast_init, port, CALL_MAIN); - RTE_LCORE_FOREACH_WORKER(cid) { - if ((ret = rte_eal_wait_lcore(cid)) < 0) { - RTE_LOG(WARNING, NETIF, "%s: lcore %d: multicast address add failed for device %s\n", - __func__, cid, port->name); - return ret; - } + if ((ret = idev_add_mcast_init(port)) != EDPVS_OK) { + RTE_LOG(WARNING, NETIF, "%s: idev_add_mcast_init failed -- %d(%s)\n", + __func__, ret, dpvs_strerror(ret)); + return ret; } /* update rss reta */ @@ -4096,25 +4094,6 @@ int netif_port_stop(struct netif_port *port) return EDPVS_OK; } -int __netif_set_mc_list(struct netif_port *dev) -{ - if (!dev->netif_ops->op_set_mc_list) - return EDPVS_NOTSUPP; - - return dev->netif_ops->op_set_mc_list(dev); -} - -int netif_set_mc_list(struct netif_port *dev) -{ - int err; - - rte_rwlock_write_lock(&dev->dev_lock); - err = __netif_set_mc_list(dev); - rte_rwlock_write_unlock(&dev->dev_lock); - - return err; -} - int netif_port_register(struct netif_port *port) { struct netif_port *cur; diff --git a/src/netif_addr.c b/src/netif_addr.c index 9859e7810..425041ead 100644 --- a/src/netif_addr.c +++ b/src/netif_addr.c @@ -25,14 +25,15 @@ #include "netif_addr.h" #include "kni.h" -static int __netif_hw_addr_add(struct netif_hw_addr_list *list, - const struct rte_ether_addr *addr) +int __netif_hw_addr_add(struct netif_hw_addr_list *list, + const struct rte_ether_addr *addr, uint16_t flags) { struct netif_hw_addr *ha; list_for_each_entry(ha, &list->addrs, list) { if (eth_addr_equal(&ha->addr, addr)) { rte_atomic32_inc(&ha->refcnt); + ha->flags |= flags; return EDPVS_OK; } } @@ -43,15 +44,15 @@ static int __netif_hw_addr_add(struct netif_hw_addr_list *list, rte_ether_addr_copy(addr, &ha->addr); rte_atomic32_set(&ha->refcnt, 1); - ha->sync_cnt = 0; + ha->flags = flags; list_add_tail(&ha->list, &list->addrs); list->count++; return EDPVS_OK; } -static int __netif_hw_addr_del(struct netif_hw_addr_list *list, - const struct rte_ether_addr *addr) +int __netif_hw_addr_del(struct netif_hw_addr_list *list, + const struct rte_ether_addr *addr, uint16_t flags) { struct netif_hw_addr *ha, *n; @@ -61,6 +62,8 @@ static int __netif_hw_addr_del(struct netif_hw_addr_list *list, list_del(&ha->list); list->count--; rte_free(ha); + } else { + ha->flags &= ~flags; } return EDPVS_OK; } @@ -81,25 +84,10 @@ static int __netif_hw_addr_sync(struct netif_hw_addr_list *to, eth_addr_dump(&ha->addr, mac, sizeof(mac)); /* for debug */ if (!ha->sync_cnt) { /* not synced to lower device */ - err = __netif_hw_addr_add(to, &ha->addr); + err = __netif_hw_addr_add(to, &ha->addr, 0); if (err == EDPVS_OK) { ha->sync_cnt++; rte_atomic32_inc(&ha->refcnt); - - /* - * when sync ha from upper to lower, - * we also need sync-back to lower's Linux kni device. - * if not, when lower's kni device mc-list changed, - * it may delete "synced" ha here by mistake. - * - * note on Linux two kni devices has no relationship. - * - * the whole logic should be: - * upper.kni -> uppper -> lower -> lower.kni - */ - if (kni_dev_exist(todev)) - linux_hw_mc_add(todev->kni.name, (uint8_t *)&ha->addr); - RTE_LOG(DEBUG, NETIF, "%s: sync %s to %s OK!\n", __func__, mac, todev->name); } else { @@ -111,14 +99,10 @@ static int __netif_hw_addr_sync(struct netif_hw_addr_list *to, /* both "ha->sync_cnt != 0" and "refcnt == 1" means * lower device is the only reference of this ha. * we can "unsync" from lower dev and remove it for upper. */ - err = __netif_hw_addr_del(to, &ha->addr); + err = __netif_hw_addr_del(to, &ha->addr, 0); if (err == EDPVS_OK) { - if (kni_dev_exist(todev)) - linux_hw_mc_del(todev->kni.name, (uint8_t *)&ha->addr); - RTE_LOG(DEBUG, NETIF, "%s: unsync %s to %s OK!\n", __func__, mac, todev->name); - list_del(&ha->list); rte_free(ha); from->count--; @@ -143,7 +127,8 @@ static int __netif_hw_addr_unsync(struct netif_hw_addr_list *to, static int __netif_hw_addr_sync_multiple(struct netif_hw_addr_list *to, struct netif_hw_addr_list *from, - struct netif_port *todev) + struct netif_port *todev, + int sync_cnt) { struct netif_hw_addr *ha, *n; int err = EDPVS_OK; @@ -153,14 +138,12 @@ static int __netif_hw_addr_sync_multiple(struct netif_hw_addr_list *to, eth_addr_dump(&ha->addr, mac, sizeof(mac)); /* for debug */ if (rte_atomic32_read(&ha->refcnt) == ha->sync_cnt) { - err = __netif_hw_addr_del(to, &ha->addr); + /* 'ha->refcnt == ha->sync_cnt' means the 'ha' has been removed from currecnt device + * and all references of this ha are from lower devices, so it's time to unsync. */ + err = __netif_hw_addr_del(to, &ha->addr, 0); if (err == EDPVS_OK) { - if (kni_dev_exist(todev)) - linux_hw_mc_del(todev->kni.name, (uint8_t *)&ha->addr); - RTE_LOG(DEBUG, NETIF, "%s: unsync %s to %s OK!\n", __func__, mac, todev->name); - ha->sync_cnt--; if (rte_atomic32_dec_and_test(&ha->refcnt)) { list_del(&ha->list); @@ -172,26 +155,12 @@ static int __netif_hw_addr_sync_multiple(struct netif_hw_addr_list *to, __func__, mac, todev->name); break; } - } else { - err = __netif_hw_addr_add(to, &ha->addr); + } else if (ha->sync_cnt < sync_cnt) { + /* sync to lower devices only once */ + err = __netif_hw_addr_add(to, &ha->addr, 0); if (err == EDPVS_OK) { ha->sync_cnt++; rte_atomic32_inc(&ha->refcnt); - - /* - * when sync ha from upper to lower, - * we also need sync-back to lower's Linux kni device. - * if not, when lower's kni device mc-list changed, - * it may delete "synced" ha here by mistake. - * - * note on Linux two kni devices has no relationship. - * - * the whole logic should be: - * upper.kni -> uppper -> lower -> lower.kni - */ - if (kni_dev_exist(todev)) - linux_hw_mc_add(todev->kni.name, (uint8_t *)&ha->addr); - RTE_LOG(DEBUG, NETIF, "%s: sync %s to %s OK!\n", __func__, mac, todev->name); } else { @@ -204,20 +173,30 @@ static int __netif_hw_addr_sync_multiple(struct netif_hw_addr_list *to, } static int __netif_hw_addr_unsync_multiple(struct netif_hw_addr_list *to, - struct netif_hw_addr_list *from) + struct netif_hw_addr_list *from, + int sync_cnt) { /* TODO: */ return EDPVS_INVAL; } -int __netif_mc_add(struct netif_port *dev, const struct rte_ether_addr *addr) +int __netif_set_mc_list(struct netif_port *dev) { - return __netif_hw_addr_add(&dev->mc, addr); + if (!dev->netif_ops->op_set_mc_list) + return EDPVS_NOTSUPP; + + return dev->netif_ops->op_set_mc_list(dev); } -int __netif_mc_del(struct netif_port *dev, const struct rte_ether_addr *addr) +int netif_set_mc_list(struct netif_port *dev) { - return __netif_hw_addr_del(&dev->mc, addr); + int err; + + rte_rwlock_write_lock(&dev->dev_lock); + err = __netif_set_mc_list(dev); + rte_rwlock_write_unlock(&dev->dev_lock); + + return err; } int netif_mc_add(struct netif_port *dev, const struct rte_ether_addr *addr) @@ -225,7 +204,7 @@ int netif_mc_add(struct netif_port *dev, const struct rte_ether_addr *addr) int err; rte_rwlock_write_lock(&dev->dev_lock); - err = __netif_mc_add(dev, addr); + err = __netif_hw_addr_add(&dev->mc, addr, 0); if (err == EDPVS_OK) err = __netif_set_mc_list(dev); rte_rwlock_write_unlock(&dev->dev_lock); @@ -238,7 +217,7 @@ int netif_mc_del(struct netif_port *dev, const struct rte_ether_addr *addr) int err; rte_rwlock_write_lock(&dev->dev_lock); - err = __netif_mc_del(dev, addr); + err = __netif_hw_addr_del(&dev->mc, addr, 0); if (err == EDPVS_OK) err = __netif_set_mc_list(dev); rte_rwlock_write_unlock(&dev->dev_lock); @@ -271,8 +250,8 @@ void netif_mc_init(struct netif_port *dev) rte_rwlock_write_unlock(&dev->dev_lock); } -int __netif_mc_dump(struct netif_port *dev, - struct rte_ether_addr *addrs, size_t *naddr) +int __netif_mc_dump(struct netif_port *dev, uint16_t filter_flags, + struct rte_ether_addr *addrs, size_t *naddr) { struct netif_hw_addr *ha; int off = 0; @@ -281,42 +260,74 @@ int __netif_mc_dump(struct netif_port *dev, return EDPVS_NOROOM; list_for_each_entry(ha, &dev->mc.addrs, list) - rte_ether_addr_copy(&ha->addr, &addrs[off++]); + if (!filter_flags || ha->flags & filter_flags) + rte_ether_addr_copy(&ha->addr, &addrs[off++]); *naddr = off; return EDPVS_OK; } -int netif_mc_dump(struct netif_port *dev, - struct rte_ether_addr *addrs, size_t *naddr) +int netif_mc_dump(struct netif_port *dev, uint16_t filter_flags, + struct rte_ether_addr *addrs, size_t *naddr) { int err; rte_rwlock_read_lock(&dev->dev_lock); - err = __netif_mc_dump(dev, addrs, naddr); + err = __netif_mc_dump(dev, filter_flags, addrs, naddr); rte_rwlock_read_unlock(&dev->dev_lock); return err; } +/* only used in __netif_mc_dump_all */ +struct netif_hw_addr_entry { + struct rte_ether_addr addr; + uint32_t refcnt; + uint16_t flags; + uint16_t sync_cnt; +}; + +static int __netif_mc_dump_all(struct netif_port *dev, uint16_t filter_flags, + struct netif_hw_addr_entry *addrs, size_t *naddr) +{ + struct netif_hw_addr *ha; + int off = 0; + + if (*naddr < dev->mc.count) + return EDPVS_NOROOM; + + list_for_each_entry(ha, &dev->mc.addrs, list) { + rte_ether_addr_copy(&ha->addr, &addrs[off].addr); + addrs[off].refcnt = rte_atomic32_read(&ha->refcnt); + addrs[off].flags = ha->flags; + addrs[off].sync_cnt = ha->sync_cnt; + off++; + } + + *naddr = off; + return EDPVS_OK; +} + int __netif_mc_print(struct netif_port *dev, char *buf, int *len, int *pnaddr) { - struct rte_ether_addr addrs[NETIF_MAX_HWADDR]; + struct netif_hw_addr_entry addrs[NETIF_MAX_HWADDR]; size_t naddr = NELEMS(addrs); int err, i; int strlen = 0; - err = __netif_mc_dump(dev, addrs, &naddr); + err = __netif_mc_dump_all(dev, 0, addrs, &naddr); if (err != EDPVS_OK) goto errout; for (i = 0; i < naddr && *len > strlen; i++) { err = snprintf(buf + strlen, *len - strlen, - " link %02x:%02x:%02x:%02x:%02x:%02x\n", - addrs[i].addr_bytes[0], addrs[i].addr_bytes[1], - addrs[i].addr_bytes[2], addrs[i].addr_bytes[3], - addrs[i].addr_bytes[4], addrs[i].addr_bytes[5]); + " link %02x:%02x:%02x:%02x:%02x:%02x %srefcnt %d, synced %d\n", + addrs[i].addr.addr_bytes[0], addrs[i].addr.addr_bytes[1], + addrs[i].addr.addr_bytes[2], addrs[i].addr.addr_bytes[3], + addrs[i].addr.addr_bytes[4], addrs[i].addr.addr_bytes[5], + addrs[i].flags & HW_ADDR_F_FROM_KNI ? "(kni) ": "", + addrs[i].refcnt, addrs[i].sync_cnt); if (err < 0) { err = EDPVS_NOROOM; goto errout; @@ -400,11 +411,11 @@ int netif_mc_unsync(struct netif_port *to, struct netif_port *from) return err; } -int __netif_mc_sync_multiple(struct netif_port *to, struct netif_port *from) +int __netif_mc_sync_multiple(struct netif_port *to, struct netif_port *from, int sync_cnt) { int err; - err = __netif_hw_addr_sync_multiple(&to->mc, &from->mc, to); + err = __netif_hw_addr_sync_multiple(&to->mc, &from->mc, to, sync_cnt); if (err == EDPVS_OK) err = __netif_set_mc_list(to); @@ -412,14 +423,15 @@ int __netif_mc_sync_multiple(struct netif_port *to, struct netif_port *from) } int netif_mc_sync_multiple(struct netif_port *to, - struct netif_port *from) + struct netif_port *from, + int sync_cnt) { int err; rte_rwlock_write_lock(&to->dev_lock); rte_rwlock_write_lock(&from->dev_lock); - err = __netif_mc_sync_multiple(to, from); + err = __netif_mc_sync_multiple(to, from, sync_cnt); rte_rwlock_write_unlock(&from->dev_lock); rte_rwlock_write_unlock(&to->dev_lock); @@ -427,25 +439,27 @@ int netif_mc_sync_multiple(struct netif_port *to, return err; } -int __netif_mc_unsync_multiple(struct netif_port *to, struct netif_port *from) +int __netif_mc_unsync_multiple(struct netif_port *to, + struct netif_port *from, + int sync_cnt) { int err; - err = __netif_hw_addr_unsync_multiple(&to->mc, &from->mc); + err = __netif_hw_addr_unsync_multiple(&to->mc, &from->mc, sync_cnt); if (err == EDPVS_OK) err = __netif_set_mc_list(to); return err; } -int netif_mc_unsync_multiple(struct netif_port *to, struct netif_port *from) +int netif_mc_unsync_multiple(struct netif_port *to, struct netif_port *from, int sync_cnt) { int err; rte_rwlock_write_lock(&to->dev_lock); rte_rwlock_write_lock(&from->dev_lock); - err = __netif_mc_unsync_multiple(to, from); + err = __netif_mc_unsync_multiple(to, from, sync_cnt); rte_rwlock_write_unlock(&from->dev_lock); rte_rwlock_write_unlock(&to->dev_lock); From 5b2fc59574489c6909efd45b856ff853e787d85d Mon Sep 17 00:00:00 2001 From: ywc689 Date: Wed, 10 Jul 2024 11:32:10 +0800 Subject: [PATCH 39/63] dpip: add 'maddr' subcommand to show multicast addresses Signed-off-by: ywc689 --- include/conf/inetaddr.h | 13 +++ include/conf/netif_addr.h | 37 +++++++++ include/conf/sockopts.h | 2 + include/inetaddr.h | 2 + include/netif_addr.h | 7 +- src/inetaddr.c | 139 +++++++++++++++++++++++-------- src/kni.c | 2 +- src/netif.c | 10 +++ src/netif_addr.c | 48 +++++++---- tools/dpip/Makefile | 2 +- tools/dpip/dpip.c | 2 +- tools/dpip/link.c | 6 +- tools/dpip/maddr.c | 168 ++++++++++++++++++++++++++++++++++++++ 13 files changed, 380 insertions(+), 58 deletions(-) create mode 100644 include/conf/netif_addr.h create mode 100644 tools/dpip/maddr.c diff --git a/include/conf/inetaddr.h b/include/conf/inetaddr.h index f97994f86..7a82b0d94 100644 --- a/include/conf/inetaddr.h +++ b/include/conf/inetaddr.h @@ -100,4 +100,17 @@ struct inet_addr_front { }; #endif /* CONFIG_DPVS_AGENT */ +struct inet_maddr_entry { + char ifname[IFNAMSIZ]; + union inet_addr maddr; + int af; + uint32_t flags; + uint32_t refcnt; +} __attribute__((__packed__)); + +struct inet_maddr_array { + int nmaddr; + struct inet_maddr_entry maddrs[0]; +} __attribute__((__packed__)); + #endif /* __DPVS_INETADDR_CONF_H__ */ diff --git a/include/conf/netif_addr.h b/include/conf/netif_addr.h new file mode 100644 index 000000000..1a127e871 --- /dev/null +++ b/include/conf/netif_addr.h @@ -0,0 +1,37 @@ +/* + * DPVS is a software load balancer (Virtual Server) based on DPDK. + * + * Copyright (C) 2021 iQIYI (www.iqiyi.com). + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +#ifndef __DPVS_NETIF_ADDR_CONF_H__ +#define __DPVS_NETIF_ADDR_CONF_H__ + +enum { + HW_ADDR_F_FROM_KNI = 1, // from linux kni device in local layer +}; + +struct netif_hw_addr_entry { + char addr[18]; + uint32_t refcnt; + uint16_t flags; + uint16_t sync_cnt; +} __attribute__((__packed__)); + +struct netif_hw_addr_array { + int count; + struct netif_hw_addr_entry entries[0]; +} __attribute__((__packed__)); + +#endif diff --git a/include/conf/sockopts.h b/include/conf/sockopts.h index 2e3cd7595..83d02ccc9 100644 --- a/include/conf/sockopts.h +++ b/include/conf/sockopts.h @@ -84,6 +84,7 @@ DPVSMSG(SOCKOPT_SET_IFADDR_SET) \ DPVSMSG(SOCKOPT_SET_IFADDR_FLUSH) \ DPVSMSG(SOCKOPT_GET_IFADDR_SHOW) \ + DPVSMSG(SOCKOPT_GET_IFMADDR_SHOW) \ \ DPVSMSG(SOCKOPT_NETIF_SET_LCORE) \ DPVSMSG(SOCKOPT_NETIF_SET_PORT) \ @@ -98,6 +99,7 @@ DPVSMSG(SOCKOPT_NETIF_GET_PORT_XSTATS) \ DPVSMSG(SOCKOPT_NETIF_GET_PORT_EXT_INFO) \ DPVSMSG(SOCKOPT_NETIF_GET_BOND_STATUS) \ + DPVSMSG(SOCKOPT_NETIF_GET_MADDR)\ DPVSMSG(SOCKOPT_NETIF_GET_MAX) \ \ DPVSMSG(SOCKOPT_SET_NEIGH_ADD) \ diff --git a/include/inetaddr.h b/include/inetaddr.h index 2a85e7611..44276659f 100644 --- a/include/inetaddr.h +++ b/include/inetaddr.h @@ -31,10 +31,12 @@ struct inet_device { struct list_head ifa_list[DPVS_MAX_LCORE]; /* inet_ifaddr list */ struct list_head ifm_list[DPVS_MAX_LCORE]; /* inet_ifmcaddr list*/ uint32_t ifa_cnt[DPVS_MAX_LCORE]; + uint32_t ifm_cnt[DPVS_MAX_LCORE]; rte_atomic32_t refcnt; /* not used yet */ #define this_ifa_list ifa_list[rte_lcore_id()] #define this_ifm_list ifm_list[rte_lcore_id()] #define this_ifa_cnt ifa_cnt[rte_lcore_id()] +#define this_ifm_cnt ifm_cnt[rte_lcore_id()] }; /* diff --git a/include/netif_addr.h b/include/netif_addr.h index c90da57cc..e1d98aa8e 100644 --- a/include/netif_addr.h +++ b/include/netif_addr.h @@ -24,9 +24,7 @@ #ifndef __DPVS_NETIF_ADDR_H__ #define __DPVS_NETIF_ADDR_H__ -enum { - HW_ADDR_F_FROM_KNI = 1, // from linux kni device in local layer -}; +#include "conf/netif_addr.h" struct netif_hw_addr { struct list_head list; @@ -55,6 +53,7 @@ int netif_mc_add(struct netif_port *dev, const struct rte_ether_addr *addr); int netif_mc_del(struct netif_port *dev, const struct rte_ether_addr *addr); void netif_mc_flush(struct netif_port *dev); void netif_mc_init(struct netif_port *dev); + int __netif_mc_dump(struct netif_port *dev, uint16_t filter_flags, struct rte_ether_addr *addrs, size_t *naddr); int netif_mc_dump(struct netif_port *dev, uint16_t filter_flags, @@ -74,6 +73,8 @@ int netif_mc_sync_multiple(struct netif_port *to, struct netif_port *from, int s int __netif_mc_unsync_multiple(struct netif_port *to, struct netif_port *from, int sync_cnt); int netif_mc_unsync_multiple(struct netif_port *to, struct netif_port *from, int sync_cnt); +int netif_get_multicast_addrs(struct netif_port *dev, void **out, size_t *outlen); + static inline int eth_addr_equal(const struct rte_ether_addr *addr1, const struct rte_ether_addr *addr2) { diff --git a/src/inetaddr.c b/src/inetaddr.c index c5f13e0b6..077f9d844 100644 --- a/src/inetaddr.c +++ b/src/inetaddr.c @@ -91,6 +91,7 @@ static inline void imc_hash(struct inet_ifmcaddr *imc, struct inet_device *idev) { list_add(&imc->d_list, &idev->this_ifm_list); ++imc->refcnt; + ++idev->this_ifm_cnt; } static inline void imc_unhash(struct inet_ifmcaddr *imc) @@ -99,6 +100,7 @@ static inline void imc_unhash(struct inet_ifmcaddr *imc) list_del(&imc->d_list); --imc->refcnt; + --imc->idev->this_ifm_cnt; } static struct inet_ifmcaddr *imc_lookup(int af, const struct inet_device *idev, @@ -1833,6 +1835,39 @@ static int ifaddr_get_verbose(struct inet_device *idev, struct inet_addr_data_ar return err; } +static int ifmaddr_fill_entries(struct inet_device *idev, struct inet_maddr_array **parray, int *plen) +{ + lcoreid_t cid; + int ifm_cnt, len, off; + struct inet_ifmcaddr *ifm; + struct inet_maddr_array *array; + + cid = rte_lcore_id(); + ifm_cnt = idev->ifm_cnt[cid]; + + len = sizeof(struct inet_maddr_array) + ifm_cnt * sizeof(struct inet_maddr_entry); + array = rte_calloc(NULL, 1, len, RTE_CACHE_LINE_SIZE); + if (unlikely(!array)) + return EDPVS_NOMEM; + + off = 0; + list_for_each_entry(ifm, &idev->ifm_list[cid], d_list) { + strncpy(array->maddrs[off].ifname, ifm->idev->dev->name, + sizeof(array->maddrs[off].ifname) - 1); + array->maddrs[off].maddr = ifm->addr; + array->maddrs[off].af = ifm->af; + array->maddrs[off].flags = ifm->flags; + array->maddrs[off].refcnt = ifm->refcnt; + if (++off >= ifm_cnt) + break; + } + array->nmaddr = off; + + *parray = array; + *plen = len; + return EDPVS_OK; +} + static int ifa_sockopt_set(sockoptid_t opt, const void *conf, size_t size) { struct netif_port *dev; @@ -1967,60 +2002,92 @@ static int ifa_sockopt_get(sockoptid_t opt, const void *conf, size_t size, int err, len = 0; struct netif_port *dev; struct inet_device *idev = NULL; + struct inet_addr_data_array *array = NULL; const struct inet_addr_param *param = conf; - if (!conf || size < sizeof(struct inet_addr_param) || !out || !outsize) + struct inet_maddr_array *marray = NULL; + const char *ifname = conf; + + if (!conf || !out || !outsize) return EDPVS_INVAL; - if (opt != SOCKOPT_GET_IFADDR_SHOW) - return EDPVS_NOTSUPP; + switch (opt) { + case SOCKOPT_GET_IFADDR_SHOW: + if (size < sizeof(struct inet_addr_param) || param->ifa_ops != INET_ADDR_GET) + return EDPVS_INVAL; - if (param->ifa_ops != INET_ADDR_GET) - return EDPVS_INVAL; + if (param->ifa_entry.af != AF_INET && + param->ifa_entry.af != AF_INET6 && + param->ifa_entry.af != AF_UNSPEC) + return EDPVS_NOTSUPP; - if (param->ifa_entry.af != AF_INET && - param->ifa_entry.af != AF_INET6 && - param->ifa_entry.af != AF_UNSPEC) - return EDPVS_NOTSUPP; + if (strlen(param->ifa_entry.ifname)) { + dev = netif_port_get_by_name(param->ifa_entry.ifname); + if (!dev) { + RTE_LOG(WARNING, IFA, "%s: no such device: %s\n", + __func__, param->ifa_entry.ifname); + return EDPVS_NOTEXIST; + } + idev = dev_get_idev(dev); + if (!idev) + return EDPVS_RESOURCE; + } + + if (param->ifa_ops_flags & IFA_F_OPS_VERBOSE) + err = ifaddr_get_verbose(idev, &array, &len); + else if (param->ifa_ops_flags & IFA_F_OPS_STATS) + err = ifaddr_get_stats(idev, &array, &len); + else + err = ifaddr_get_basic(idev, &array, &len); - if (strlen(param->ifa_entry.ifname)) { - dev = netif_port_get_by_name(param->ifa_entry.ifname); + if (err != EDPVS_OK) { + RTE_LOG(WARNING, IFA, "%s: fail to get inet addresses -- %s!\n", + __func__, dpvs_strerror(err)); + return err; + } + + if (idev) + idev_put(idev); + + if (array) { + array->ops = INET_ADDR_GET; + array->ops_flags = param->ifa_ops_flags; + } + + *out = array; + *outsize = len; + break; + case SOCKOPT_GET_IFMADDR_SHOW: + if (!size || strlen(ifname) == 0) + return EDPVS_INVAL; + + dev = netif_port_get_by_name(ifname); if (!dev) { - RTE_LOG(WARNING, IFA, "%s: no such device: %s\n", - __func__, param->ifa_entry.ifname); + RTE_LOG(WARNING, IFA, "%s: no such device: %s\n", __func__, ifname); return EDPVS_NOTEXIST; } - idev = dev_get_idev(dev); if (!idev) return EDPVS_RESOURCE; - } - - if (param->ifa_ops_flags & IFA_F_OPS_VERBOSE) - err = ifaddr_get_verbose(idev, &array, &len); - else if (param->ifa_ops_flags & IFA_F_OPS_STATS) - err = ifaddr_get_stats(idev, &array, &len); - else - err = ifaddr_get_basic(idev, &array, &len); - if (err != EDPVS_OK) { - RTE_LOG(WARNING, IFA, "%s: fail to get inet addresses -- %s!\n", - __func__, dpvs_strerror(err)); - return err; - } + err = ifmaddr_fill_entries(idev, &marray, &len); + if (err != EDPVS_OK) { + RTE_LOG(WARNING, IFA, "%s: fail to get inet maddresses -- %s!\n", + __func__, dpvs_strerror(err)); + return err; + } - if (idev) idev_put(idev); - if (array) { - array->ops = INET_ADDR_GET; - array->ops_flags = param->ifa_ops_flags; + *out = marray; + *outsize = len; + break; + default: + *out = NULL; + *outsize = 0; + return EDPVS_NOTSUPP; } - - *out = array; - *outsize = len; - return EDPVS_OK; } @@ -2067,7 +2134,7 @@ static struct dpvs_sockopts ifa_sockopts = { .set_opt_max = SOCKOPT_SET_IFADDR_FLUSH, .set = ifa_sockopt_set, .get_opt_min = SOCKOPT_GET_IFADDR_SHOW, - .get_opt_max = SOCKOPT_GET_IFADDR_SHOW, + .get_opt_max = SOCKOPT_GET_IFMADDR_SHOW, .get = ifa_sockopt_get, }; diff --git a/src/kni.c b/src/kni.c index b6af78ceb..185371d42 100644 --- a/src/kni.c +++ b/src/kni.c @@ -34,7 +34,7 @@ #include "conf/common.h" #include "dpdk.h" #include "netif.h" -#include "netif_addr.h" +#include "conf/netif_addr.h" #include "ctrl.h" #include "kni.h" #include "vlan.h" diff --git a/src/netif.c b/src/netif.c index 4bc01a1bb..a40252f21 100644 --- a/src/netif.c +++ b/src/netif.c @@ -28,6 +28,7 @@ #include "conf/common.h" #include "netif.h" #include "netif_addr.h" +#include "conf/netif_addr.h" #include "vlan.h" #include "ctrl.h" #include "list.h" @@ -5180,6 +5181,15 @@ static int netif_sockopt_get(sockoptid_t opt, const void *in, size_t inlen, return EDPVS_NOTEXIST; ret = get_bond_status(port, out, outlen); break; + case SOCKOPT_NETIF_GET_MADDR: + if (!in) + return EDPVS_INVAL; + name = (char *)in; + port = netif_port_get_by_name(name); + if (!port) + return EDPVS_NOTEXIST; + ret = netif_get_multicast_addrs(port, out, outlen); + break; default: RTE_LOG(WARNING, NETIF, "[%s] invalid netif get cmd: %d\n", __func__, opt); diff --git a/src/netif_addr.c b/src/netif_addr.c index 425041ead..41bd0080f 100644 --- a/src/netif_addr.c +++ b/src/netif_addr.c @@ -23,6 +23,7 @@ */ #include "netif.h" #include "netif_addr.h" +#include "conf/netif_addr.h" #include "kni.h" int __netif_hw_addr_add(struct netif_hw_addr_list *list, @@ -279,16 +280,8 @@ int netif_mc_dump(struct netif_port *dev, uint16_t filter_flags, return err; } -/* only used in __netif_mc_dump_all */ -struct netif_hw_addr_entry { - struct rte_ether_addr addr; - uint32_t refcnt; - uint16_t flags; - uint16_t sync_cnt; -}; - static int __netif_mc_dump_all(struct netif_port *dev, uint16_t filter_flags, - struct netif_hw_addr_entry *addrs, size_t *naddr) + struct netif_hw_addr_entry *addrs, int *naddr) { struct netif_hw_addr *ha; int off = 0; @@ -297,7 +290,7 @@ static int __netif_mc_dump_all(struct netif_port *dev, uint16_t filter_flags, return EDPVS_NOROOM; list_for_each_entry(ha, &dev->mc.addrs, list) { - rte_ether_addr_copy(&ha->addr, &addrs[off].addr); + eth_addr_dump(&ha->addr, addrs[off].addr, sizeof(addrs[off].addr)); addrs[off].refcnt = rte_atomic32_read(&ha->refcnt); addrs[off].flags = ha->flags; addrs[off].sync_cnt = ha->sync_cnt; @@ -312,7 +305,7 @@ int __netif_mc_print(struct netif_port *dev, char *buf, int *len, int *pnaddr) { struct netif_hw_addr_entry addrs[NETIF_MAX_HWADDR]; - size_t naddr = NELEMS(addrs); + int naddr = NELEMS(addrs); int err, i; int strlen = 0; @@ -322,10 +315,8 @@ int __netif_mc_print(struct netif_port *dev, for (i = 0; i < naddr && *len > strlen; i++) { err = snprintf(buf + strlen, *len - strlen, - " link %02x:%02x:%02x:%02x:%02x:%02x %srefcnt %d, synced %d\n", - addrs[i].addr.addr_bytes[0], addrs[i].addr.addr_bytes[1], - addrs[i].addr.addr_bytes[2], addrs[i].addr.addr_bytes[3], - addrs[i].addr.addr_bytes[4], addrs[i].addr.addr_bytes[5], + " link %s %srefcnt %d, synced %d\n", + addrs[i].addr, addrs[i].flags & HW_ADDR_F_FROM_KNI ? "(kni) ": "", addrs[i].refcnt, addrs[i].sync_cnt); if (err < 0) { @@ -346,6 +337,33 @@ int __netif_mc_print(struct netif_port *dev, return err; } +int netif_get_multicast_addrs(struct netif_port *dev, void **out, size_t *outlen) +{ + int err; + size_t len; + struct netif_hw_addr_array *array; + + rte_rwlock_read_lock(&dev->dev_lock); + len = sizeof(*array) + dev->mc.count * sizeof(struct netif_hw_addr_entry); + array = rte_zmalloc(NULL, len, RTE_CACHE_LINE_SIZE); + if (unlikely(!array)) { + err = EDPVS_NOMEM; + } else { + array->count = dev->mc.count; + err = __netif_mc_dump_all(dev, 0, array->entries, &array->count); + } + rte_rwlock_read_unlock(&dev->dev_lock); + + if (err != EDPVS_OK) { + *out = NULL; + *outlen = 0; + } else { + *out = array; + *outlen = len; + } + return err; +} + int netif_mc_print(struct netif_port *dev, char *buf, int *len, int *pnaddr) { diff --git a/tools/dpip/Makefile b/tools/dpip/Makefile index e1bbe21e4..4dc648e38 100644 --- a/tools/dpip/Makefile +++ b/tools/dpip/Makefile @@ -39,7 +39,7 @@ DEFS = -D DPVS_MAX_LCORE=64 -D DPIP_VERSION=\"$(VERSION_STRING)\" CFLAGS += $(DEFS) -OBJS = ipset.o dpip.o utils.o route.o addr.o neigh.o link.o vlan.o \ +OBJS = ipset.o dpip.o utils.o route.o addr.o neigh.o link.o vlan.o maddr.o \ qsch.o cls.o tunnel.o ipset.o ipv6.o iftraf.o eal_mem.o flow.o \ ../../src/common.o ../keepalived/keepalived/check/sockopt.o diff --git a/tools/dpip/dpip.c b/tools/dpip/dpip.c index 596a534f5..a3b31f1c6 100644 --- a/tools/dpip/dpip.c +++ b/tools/dpip/dpip.c @@ -35,7 +35,7 @@ static void usage(void) " "DPIP_NAME" [OPTIONS] OBJECT { COMMAND | help }\n" "Parameters:\n" " OBJECT := { link | addr | route | neigh | vlan | tunnel | qsch | cls |\n" - " ipv6 | iftraf | eal-mem | ipset | flow }\n" + " ipv6 | iftraf | eal-mem | ipset | flow | maddr }\n" " COMMAND := { create | destroy | add | del | show (list) | set (change) |\n" " replace | flush | test | enable | disable }\n" "Options:\n" diff --git a/tools/dpip/link.c b/tools/dpip/link.c index f2d3f7798..fd3d5bd84 100644 --- a/tools/dpip/link.c +++ b/tools/dpip/link.c @@ -101,8 +101,10 @@ static inline int get_netif_port_list(void) if (g_nic_list) free(g_nic_list); g_nic_list = calloc(1, len); - if (!g_nic_list) + if (!g_nic_list) { + dpvs_sockopt_msg_free((void *)p_port_list); return EDPVS_NOMEM; + } memcpy(g_nic_list, p_port_list, len); @@ -501,6 +503,7 @@ static int dump_bond_status(char *name, int namelen) p_get->link_up_prop_delay); if (p_get->slave_nb > NETIF_MAX_BOND_SLAVES) { printf("too many slaves: %d\n", p_get->slave_nb); + dpvs_sockopt_msg_free(p_get); return EDPVS_INVAL; } for (i = 0; i < p_get->slave_nb; i++) { @@ -514,6 +517,7 @@ static int dump_bond_status(char *name, int namelen) } printf("\n"); + dpvs_sockopt_msg_free(p_get); return EDPVS_OK; } diff --git a/tools/dpip/maddr.c b/tools/dpip/maddr.c new file mode 100644 index 000000000..7bb82a544 --- /dev/null +++ b/tools/dpip/maddr.c @@ -0,0 +1,168 @@ +/* + * DPVS is a software load balancer (Virtual Server) based on DPDK. + * + * Copyright (C) 2021 iQIYI (www.iqiyi.com). + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +#include "dpip.h" +#include "conf/sockopts.h" +#include "conf/common.h" +#include "conf/netif.h" +#include "conf/netif_addr.h" +#include "conf/inetaddr.h" +#include "sockopt.h" + +static void maddr_help(void) +{ + fprintf(stderr, "Usage: dpip maddr show [dev STRING]\n"); +} + +static int maddr_parse_args(struct dpip_conf *conf, char *ifname, size_t len) +{ + while (conf->argc > 0) { + if (strcmp(conf->argv[0], "dev") == 0) { + NEXTARG_CHECK(conf, "dev"); + snprintf(ifname, len, "%s", conf->argv[0]); + } + NEXTARG(conf); + } + + if (conf->argc > 0) { + fprintf(stderr, "too many arguments\n"); + return -1; + } + + return 0; +} + +static int hwm_get_and_dump(const char *ifname, size_t len, bool verbose) +{ + int i, err; + size_t outlen; + struct netif_hw_addr_array *out; + struct netif_hw_addr_entry *entry; + + err = dpvs_getsockopt(SOCKOPT_NETIF_GET_MADDR, ifname, len, (void **)&out, &outlen); + if (err != EDPVS_OK || !out || !outlen) + return err; + + for (i = 0; i < out->count; i++) { + entry = &out->entries[i]; + if (verbose) { + printf("\tlink %s%s\t\trefcnt %u\t\tsync %d\n", + entry->addr, entry->flags & HW_ADDR_F_FROM_KNI ? " (+kni)" : "", + entry->refcnt, entry->sync_cnt); + } else { + printf("\tlink %s\n", entry->addr); + } + } + + dpvs_sockopt_msg_free(out); + return EDPVS_OK; +} + +static int ifm_get_and_dump(const char *ifname, size_t len, bool verbose) +{ + int i, err; + size_t outlen; + struct inet_maddr_array *out; + struct inet_maddr_entry *entry; + char ipbuf[64]; + + err = dpvs_getsockopt(SOCKOPT_GET_IFMADDR_SHOW, ifname, len, (void **)&out, &outlen); + if (err != EDPVS_OK || !out || !outlen) + return err; + + for (i = 0; i < out->nmaddr; i++) { + entry = &out->maddrs[i]; + if (verbose) { + printf("\t%5s %s\t\tflags 0x%x\t\trefcnt %u\n", entry->af == AF_INET6 ? "inet6" : "inet", + inet_ntop(entry->af, &entry->maddr, ipbuf, sizeof(ipbuf)) ? ipbuf : "unknown", + entry->flags, entry->refcnt); + } else { + printf("\t%5s %s\n", entry->af == AF_INET6 ? "inet6" : "inet", + inet_ntop(entry->af, &entry->maddr, ipbuf, sizeof(ipbuf)) ? ipbuf : "unknown"); + } + } + + dpvs_sockopt_msg_free(out); + return EDPVS_OK; +} + +static int maddr_get_and_dump(const char *ifname, size_t len, bool verbose) +{ + int err; + + err = hwm_get_and_dump(ifname, len, verbose); + if (err != EDPVS_OK) + return err; + + return ifm_get_and_dump(ifname, len, verbose); +} + +static int maddr_do_cmd(struct dpip_obj *obj, dpip_cmd_t cmd, + struct dpip_conf *conf) +{ + int i, err; + size_t len; + char ifname[IFNAMSIZ] = { 0 }; + netif_nic_list_get_t *ports; + + if (maddr_parse_args(conf, ifname, sizeof(ifname)) != 0) + return EDPVS_INVAL; + + switch (conf->cmd) { + case DPIP_CMD_SHOW: + if (strlen(ifname) > 0) { + printf("%s:\n", ifname); + return maddr_get_and_dump(ifname, sizeof(ifname), conf->verbose); + } + + /* list all devices */ + err = dpvs_getsockopt(SOCKOPT_NETIF_GET_PORT_LIST, NULL, 0, (void **)&ports, &len); + if (err != EDPVS_OK || !ports || !len) + return err; + for (i = 0; i < ports->nic_num && i < NETIF_MAX_PORTS; i++) { + printf("%d:\t%s\n", ports->idname[i].id + 1, ports->idname[i].name); + err = maddr_get_and_dump(ports->idname[i].name, + sizeof(ports->idname[i].name), conf->verbose); + if (err != EDPVS_OK) { + dpvs_sockopt_msg_free(ports); + return err; + } + } + dpvs_sockopt_msg_free(ports); + break; + default: + return EDPVS_NOTSUPP; + } + + return EDPVS_OK; +} + +struct dpip_obj dpip_maddr = { + .name = "maddr", + .help = maddr_help, + .do_cmd = maddr_do_cmd, +}; + +static void __init maddr_init(void) +{ + dpip_register_obj(&dpip_maddr); +} + +static void __exit maddr_exit(void) +{ + dpip_unregister_obj(&dpip_maddr); +} From 922315b2180589e04b188546c1524608d4795043 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Wed, 24 Jul 2024 14:33:12 +0800 Subject: [PATCH 40/63] lldp: add supports for lldp protocol Signed-off-by: ywc689 --- conf/dpvs.bond.conf.sample | 1 + conf/dpvs.conf.items | 1 + conf/dpvs.conf.sample | 1 + conf/dpvs.conf.single-bond.sample | 1 + conf/dpvs.conf.single-nic.sample | 1 + include/conf/common.h | 38 + include/conf/lldp.h | 41 + include/conf/netif.h | 3 + include/conf/sockopts.h | 3 + include/ctrl.h | 1 + include/lldp.h | 118 ++ include/mbuf.h | 1 + include/netif.h | 9 +- include/timer.h | 1 + src/common.c | 216 ++++ src/ctrl.c | 2 +- src/global_conf.c | 27 +- src/inet.c | 5 + src/ip_tunnel.c | 1 + src/lldp.c | 1894 +++++++++++++++++++++++++++++ src/main.c | 1 + src/mbuf.c | 5 + src/netif.c | 35 +- src/vlan.c | 1 + tools/dpip/Makefile | 2 +- tools/dpip/dpip.c | 2 +- tools/dpip/link.c | 26 +- tools/dpip/lldp.c | 128 ++ 28 files changed, 2556 insertions(+), 9 deletions(-) create mode 100644 include/conf/lldp.h create mode 100644 include/lldp.h create mode 100644 src/lldp.c create mode 100644 tools/dpip/lldp.c diff --git a/conf/dpvs.bond.conf.sample b/conf/dpvs.bond.conf.sample index d8cc1e5e8..d35d10328 100644 --- a/conf/dpvs.bond.conf.sample +++ b/conf/dpvs.bond.conf.sample @@ -17,6 +17,7 @@ global_defs { ! log_async_mode off ! kni on ! pdump off + lldp on } ! netif config diff --git a/conf/dpvs.conf.items b/conf/dpvs.conf.items index 5b5f9b35e..448b40044 100644 --- a/conf/dpvs.conf.items +++ b/conf/dpvs.conf.items @@ -19,6 +19,7 @@ global_defs { log_async_pool_size 16383 <16383, 1023-unlimited> pdump off kni on + lldp on } ! netif config diff --git a/conf/dpvs.conf.sample b/conf/dpvs.conf.sample index 14b0846d5..002aab56f 100644 --- a/conf/dpvs.conf.sample +++ b/conf/dpvs.conf.sample @@ -17,6 +17,7 @@ global_defs { ! log_async_mode on ! kni on ! pdump off + lldp on } ! netif config diff --git a/conf/dpvs.conf.single-bond.sample b/conf/dpvs.conf.single-bond.sample index 3fdfbfd33..b0c1c375a 100644 --- a/conf/dpvs.conf.single-bond.sample +++ b/conf/dpvs.conf.single-bond.sample @@ -16,6 +16,7 @@ global_defs { ! log_file /var/log/dpvs.log ! log_async_mode on ! kni on + lldp on } ! netif config diff --git a/conf/dpvs.conf.single-nic.sample b/conf/dpvs.conf.single-nic.sample index 3717ed07b..bb9ce994e 100644 --- a/conf/dpvs.conf.single-nic.sample +++ b/conf/dpvs.conf.single-nic.sample @@ -16,6 +16,7 @@ global_defs { ! log_file /var/log/dpvs.log ! log_async_mode on ! kni on + lldp on } ! netif config diff --git a/include/conf/common.h b/include/conf/common.h index 7472ad8f1..14a048374 100644 --- a/include/conf/common.h +++ b/include/conf/common.h @@ -22,6 +22,7 @@ #include #include #include +#include #include typedef uint32_t sockoptid_t; @@ -142,6 +143,7 @@ int linux_get_link_status(const char *ifname, int *if_flags, char *if_flags_str, int linux_set_if_mac(const char *ifname, const unsigned char mac[ETH_ALEN]); int linux_hw_mc_add(const char *ifname, const uint8_t hwma[ETH_ALEN]); int linux_hw_mc_del(const char *ifname, const uint8_t hwma[ETH_ALEN]); +int linux_ifname2index(const char *ifname); /* read "n" bytes from a descriptor */ ssize_t readn(int fd, void *vptr, size_t n); @@ -166,4 +168,40 @@ static inline char *strlwr(char *str) { return str; } +/* convert hexadecimal string to binary sequence, return the converted binary length + * note: buflen should be half in size of len at least */ +int hexstr2binary(const char *hexstr, size_t len, uint8_t *buf, size_t buflen); + +/* convert binary sequence to hexadecimal string, return the converted string length + * note: buflen should be twice in size of len at least */ +int binary2hexstr(const uint8_t *hex, size_t len, char *buf, size_t buflen); + +/* convert binary sequence to printable or hexadecimal string, return the converted string length + * note: buflen should be triple in size of len in the worst case */ +int binary2print(const uint8_t *hex, size_t len, char *buf, size_t buflen); + +/* get prefix from network mask */ +int mask2prefix(const struct sockaddr *addr); + +/* get host addresses and corresponding interfaces + * + * Loopback addresses, ipv6 link local addresses, and addresses on linked-down + * or not-running interface are ignored. If multiple addresses matched, return + * the address of the least prefix length. + * + * Params: + * @ifname: preferred interface where to get host address, can be NULL + * @result4: store ipv4 address found, can be NULL + * @result6: store ipv6 address found, can be NULL + * @ifname4: interface name of ipv4 address, can be NULL + * @ifname6: interface name of ipv6 address, can be NULL + * Return: + * 1: only ipv4 address found + * 2: only ipv6 address found + * 3: both ipv4 and ipv6 address found + * dpvs error code: error occurred + * */ +int get_host_addr(const char *ifname, struct sockaddr_storage *result4, + struct sockaddr_storage *result6, char *ifname4, char *ifname6); + #endif /* __DPVS_COMMON_H__ */ diff --git a/include/conf/lldp.h b/include/conf/lldp.h new file mode 100644 index 000000000..24f8b7831 --- /dev/null +++ b/include/conf/lldp.h @@ -0,0 +1,41 @@ +/* + * DPVS is a software load balancer (Virtual Server) based on DPDK. + * + * Copyright (C) 2021 iQIYI (www.iqiyi.com). + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +#ifndef __DPVS_LLDP_CONF_H__ +#define __DPVS_LLDP_CONF_H__ + +#include +#include "conf/sockopts.h" + +#define LLDP_MESSAGE_LEN 4096 + +#define DPVS_LLDP_NODE_LOCAL 0 +#define DPVS_LLDP_NODE_NEIGH 1 +#define DPVS_LLDP_NODE_MAX 2 + + +struct lldp_param { + uint16_t node; /* DPVS_LLDP_NODE_xxx */ + char ifname[IFNAMSIZ]; +}; + +struct lldp_message { + struct lldp_param param; + char message[LLDP_MESSAGE_LEN]; +}; + +#endif /* __DPVS_LLDP_CONF_H__ */ diff --git a/include/conf/netif.h b/include/conf/netif.h index 7ef7def4d..da11e402a 100644 --- a/include/conf/netif.h +++ b/include/conf/netif.h @@ -111,6 +111,7 @@ typedef struct netif_nic_basic_get uint16_t ol_tx_ip_csum:1; uint16_t ol_tx_tcp_csum:1; uint16_t ol_tx_udp_csum:1; + uint16_t lldp:1; } netif_nic_basic_get_t; /* nic statistics specified by port_id */ @@ -247,6 +248,8 @@ typedef struct netif_nic_set { uint16_t tc_egress_off:1; uint16_t tc_ingress_on:1; uint16_t tc_ingress_off:1; + uint16_t lldp_on:1; + uint16_t lldp_off:1; } netif_nic_set_t; typedef struct netif_bond_set { diff --git a/include/conf/sockopts.h b/include/conf/sockopts.h index 83d02ccc9..8539f9e9d 100644 --- a/include/conf/sockopts.h +++ b/include/conf/sockopts.h @@ -102,6 +102,9 @@ DPVSMSG(SOCKOPT_NETIF_GET_MADDR)\ DPVSMSG(SOCKOPT_NETIF_GET_MAX) \ \ + DPVSMSG(SOCKOPT_SET_LLDP_TODO) \ + DPVSMSG(SOCKOPT_GET_LLDP_SHOW) \ + \ DPVSMSG(SOCKOPT_SET_NEIGH_ADD) \ DPVSMSG(SOCKOPT_SET_NEIGH_DEL) \ DPVSMSG(SOCKOPT_GET_NEIGH_SHOW) \ diff --git a/include/ctrl.h b/include/ctrl.h index 888113f89..754e38b9d 100644 --- a/include/ctrl.h +++ b/include/ctrl.h @@ -201,6 +201,7 @@ int msg_dump(const struct dpvs_msg *msg, char *buf, int len); #define MSG_TYPE_IPV6_STATS 16 #define MSG_TYPE_ROUTE6 17 #define MSG_TYPE_NEIGH_GET 18 +#define MSG_TYPE_LLDP_RECV 19 #define MSG_TYPE_IFA_GET 22 #define MSG_TYPE_IFA_SET 23 #define MSG_TYPE_IFA_SYNC 24 diff --git a/include/lldp.h b/include/lldp.h new file mode 100644 index 000000000..e4f828a24 --- /dev/null +++ b/include/lldp.h @@ -0,0 +1,118 @@ +/* + * DPVS is a software load balancer (Virtual Server) based on DPDK. + * + * Copyright (C) 2021 iQIYI (www.iqiyi.com). + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#ifndef __DPVS_LLDP_H__ +#define __DPVS_LLDP_H__ + +#define DPVS_LLDP_TYPE_MAX 128 + +/* IEEE 802.3AB Clause 9: TLV Types */ +enum { + LLDP_TYPE_END = 0, + LLDP_TYPE_CHASSIS_ID = 1, + LLDP_TYPE_PORT_ID = 2, + LLDP_TYPE_TTL = 3, + LLDP_TYPE_PORT_DESC = 4, + LLDP_TYPE_SYS_NAME = 5, + LLDP_TYPE_SYS_DESC = 6, + LLDP_TYPE_SYS_CAP = 7, + LLDP_TYPE_MNG_ADDR = 8, + LLDP_TYPE_ORG = 127, +}; +#define LLDP_TYPE_VALID(t) (((t) >= 0) && ((t) < DPVS_LLDP_TYPE_MAX)) + +/* IEEE 802.3AB Clause 9.5.2: Chassis subtypes */ +enum { + LLDP_CHASSIS_ID_RESERVED = 0, + LLDP_CHASSIS_ID_CHASSIS_COMPONENT = 1, + LLDP_CHASSIS_ID_INTERFACE_ALIAS = 2, + LLDP_CHASSIS_ID_PORT_COMPONENT = 3, + LLDP_CHASSIS_ID_MAC_ADDRESS = 4, + LLDP_CHASSIS_ID_NETWORK_ADDRESS = 5, + LLDP_CHASSIS_ID_INTERFACE_NAME = 6, + LLDP_CHASSIS_ID_LOCALLY_ASSIGNED = 7, +}; +#define LLDP_CHASSIS_ID_VALID(t) (((t) > 0) && ((t) <= 7)) + +/* IEEE 802.3AB Clause 9.5.3: Port subtype */ +enum { + LLDP_PORT_ID_RESERVED = 0, + LLDP_PORT_ID_INTERFACE_ALIAS = 1, + LLDP_PORT_ID_PORT_COMPONENT = 2, + LLDP_PORT_ID_MAC_ADDRESS = 3, + LLDP_PORT_ID_NETWORK_ADDRESS = 4, + LLDP_PORT_ID_INTERFACE_NAME = 5, + LLDP_PORT_ID_AGENT_CIRCUIT_ID = 6, + LLDP_PORT_ID_LOCALLY_ASSIGNED = 7, +}; +#define LLDP_PORT_ID_VALID(t) (((t) > 0) && ((t) <= 7)) + +/* + * IETF RFC 3232: + * http://www.iana.org/assignments/ianaaddressfamilynumbers-mib + */ +enum { + LLDP_ADDR_OTHER = 0, + LLDP_ADDR_IPV4 = 1, + LLDP_ADDR_IPV6 = 2, + LLDP_ADDR_NSAP = 3, + LLDP_ADDR_HDLC = 4, + LLDP_ADDR_BBN1822 = 5, + LLDP_ADDR_ALL802 = 6, + LLDP_ADDR_E163 = 7, + LLDP_ADDR_E164 = 8, + LLDP_ADDR_F69 = 9, + LLDP_ADDR_X121 = 10, + LLDP_ADDR_IPX = 11, + LLDP_ADDR_APPLETALK = 12, + LLDP_ADDR_DECNETIV = 13, + LLDP_ADDR_BANYANVINES = 14, + LLDP_ADDR_E164WITHNSAP = 15, + LLDP_ADDR_DNS = 16, + LLDP_ADDR_DISTINGUISHEDNAME = 17, + LLDP_ADDR_ASNUMBER = 18, + LLDP_ADDR_XTPOVERIPV4 = 19, + LLDP_ADDR_XTPOVERIPV6 = 20, + LLDP_ADDR_XTPNATIVEMODEXTP = 21, + LLDP_ADDR_FIBRECHANNELWWPN = 22, + LLDP_ADDR_FIBRECHANNELWWNN = 23, + LLDP_ADDR_GWID = 24, + LLDP_ADDR_AFI = 25, + LLDP_ADDR_RESERVED = 65535, +}; + +/* IEEE 802.1AB: Annex E, Table E.1: Organizationally Specific TLVs */ +enum { + LLDP_ORG_SPEC_PVID = 1, + LLDP_ORG_SPEC_PPVID = 2, + LLDP_ORG_SPEC_VLAN_NAME = 3, + LLDP_ORG_SPEC_PROTO_ID = 4, + LLDP_ORG_SPEC_VID_USAGE = 5, + LLDP_ORG_SPEC_MGMT_VID = 6, + LLDP_ORG_SPEC_LINK_AGGR = 7, +}; +#define LLDP_ORG_SPEC_VALID(t) (((t) > 0) && ((t) <= 7)) + +void dpvs_lldp_enable(void); +void dpvs_lldp_disable(void); +bool dpvs_lldp_is_enabled(void); + +int dpvs_lldp_init(void); +int dpvs_lldp_term(void); + +#endif diff --git a/include/mbuf.h b/include/mbuf.h index a8ccde221..7fb013a48 100644 --- a/include/mbuf.h +++ b/include/mbuf.h @@ -61,6 +61,7 @@ typedef void * mbuf_userdata_field_route_t; typedef enum { MBUF_FIELD_PROTO = 0, MBUF_FIELD_ROUTE, + MBUF_FIELD_ORIGIN_PORT, } mbuf_usedata_field_t; /** diff --git a/include/netif.h b/include/netif.h index 4347eb999..af3adeb7a 100644 --- a/include/netif.h +++ b/include/netif.h @@ -48,6 +48,7 @@ enum { NETIF_PORT_FLAG_TC_EGRESS = (0x1<<10), NETIF_PORT_FLAG_TC_INGRESS = (0x1<<11), NETIF_PORT_FLAG_NO_ARP = (0x1<<12), + NETIF_PORT_FLAG_LLDP = (0x1<<13), }; /* max tx/rx queue number for each nic */ @@ -262,11 +263,15 @@ int netif_unregister_pkt(struct pkt_type *pt); /**************************** port API ******************************/ struct netif_port* netif_port_get(portid_t id); +/* get netif by name, fail return NULL */ +struct netif_port* netif_port_get_by_name(const char *name); +bool is_physical_port(portid_t pid); +bool is_bond_port(portid_t pid); +void netif_physical_port_range(portid_t *start, portid_t *end); +void netif_bond_port_range(portid_t *start, portid_t *end); /* port_conf can be NULL for default port configure */ int netif_print_port_conf(const struct rte_eth_conf *port_conf, char *buf, int *len); int netif_print_port_queue_conf(portid_t pid, char *buf, int *len); -/* get netif by name, fail return NULL */ -struct netif_port* netif_port_get_by_name(const char *name); // function only for init or termination // int netif_port_conf_get(struct netif_port *port, struct rte_eth_conf *eth_conf); int netif_port_conf_set(struct netif_port *port, const struct rte_eth_conf *conf); diff --git a/include/timer.h b/include/timer.h index ba1bb6563..a00f09fb4 100644 --- a/include/timer.h +++ b/include/timer.h @@ -17,6 +17,7 @@ */ #ifndef __DPVS_TIMER_H__ #define __DPVS_TIMER_H__ +#include #include #include "list.h" diff --git a/src/common.c b/src/common.c index c10505cf4..8452ef806 100644 --- a/src/common.c +++ b/src/common.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -246,6 +247,26 @@ int linux_hw_mc_del(const char *ifname, const uint8_t hwma[ETH_ALEN]) return linux_hw_mc_mod(ifname, hwma, false); } +int linux_ifname2index(const char *ifname) +{ + int sockfd; + struct ifreq ifr; + + sockfd = socket(AF_INET, SOCK_DGRAM, 0); + if (sockfd < 0) + return -1; + + memset(&ifr, 0, sizeof(struct ifreq)); + strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); + if (ioctl(sockfd, SIOCGIFINDEX, &ifr) < 0) { + close(sockfd); + return -1; + } + close(sockfd); + + return ifr.ifr_ifindex; +} + ssize_t readn(int fd, void *vptr, size_t n) { size_t nleft; @@ -320,3 +341,198 @@ ssize_t sendn(int fd, const void *vptr, size_t n, int flags) return (n); } +static uint8_t hex_char2num(char hex) +{ + if (hex >= '0' && hex <= '9') + return hex - '0'; + if (hex >= 'A' && hex <= 'F') + return hex - 'A' + 10; + if (hex >= 'a' && hex <= 'f') + return hex - 'a' + 10; + return 255; +} + +int hexstr2binary(const char *hexstr, size_t len, uint8_t *buf, size_t buflen) +{ + int i, j; + + for (i = 0, j = 0; i + 1 < len && j < buflen; i += 2, j++) + buf[j] = (hex_char2num(hexstr[i]) << 4) | hex_char2num(hexstr[i+1]); + + return j; +} + +#define num2hexchar(b) (((b) > 9) ? ((b) - 0xa + 'A') : ((b) + '0')) +int binary2hexstr(const uint8_t *hex, size_t len, char *buf, size_t buflen) +{ + size_t i, j; + + for (i = 0, j = 0; i < len && j + 1 < buflen; i++, j += 2) { + buf[j] = num2hexchar((hex[i] & 0xf0) >> 4); + buf[j+1] = num2hexchar(hex[i] & 0x0f); + } + + return j; +} + +int binary2print(const uint8_t *hex, size_t len, char *buf, size_t buflen) +{ + size_t i, j; + + for (i = 0, j = 0; i < len && j < buflen; i++) { + if (isprint(hex[i])) { + buf[j++] = hex[i]; + if (j >= buflen) + break; + } else { + if (j + 2 >= buflen) + break; + buf[j] = '\\'; + buf[j+1] = num2hexchar((hex[i] & 0xf0) >> 4); + buf[j+2] = num2hexchar(hex[i] & 0x0f); + j += 2; + } + } + + return j; +} + +static int is_link_local(struct sockaddr *addr) +{ + unsigned char *addrbytes; + if (addr->sa_family == AF_INET6) { + addrbytes = (unsigned char *)(&((struct sockaddr_in6 *)addr)->sin6_addr); + return (addrbytes[0] == 0xFE) && ((addrbytes[1] & 0xC0) == 0x80); /* fe80::/10 */ + } + return 0; +} + +int mask2prefix(const struct sockaddr *addr) +{ + int i, j; + int pfxlen, addrlen; + unsigned char *mask; + + if (!addr) + return -1; + + if (addr->sa_family == AF_INET) { + mask = (unsigned char *)&((struct sockaddr_in *)addr)->sin_addr; + addrlen = 4; + } else if (addr->sa_family == AF_INET6) { + mask = (unsigned char *)&((struct sockaddr_in6 *)addr)->sin6_addr; + addrlen = 16; + } else { + return -1; + } + + pfxlen = 0; + for (i = 0; i < addrlen; i++) { + for (j = 7; j >= 0; j--) { + if (mask[i] & (1U << j)) + ++pfxlen; + else + return pfxlen; + } + } + return pfxlen; +} + +int get_host_addr(const char *ifname, struct sockaddr_storage *result4, + struct sockaddr_storage *result6, char *ifname4, char *ifname6) +{ + struct ifaddrs *ifa_head, *ifa; + int found_v4 = 0, found_v6 = 0; + int pfxlen, pfxlen_v4 = 0, pfxlen_v6 = 0; + + if (getifaddrs(&ifa_head) == -1) + return -1; + + /* addresses on ifname take precedence */ + if (ifname) { + for (ifa = ifa_head; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL) + continue; + if (ifa->ifa_flags & IFF_LOOPBACK || + !(ifa->ifa_flags & IFF_UP) || + !(ifa->ifa_flags & IFF_RUNNING)) + continue; + if (is_link_local(ifa->ifa_addr)) + continue; + if (strcmp(ifname, ifa->ifa_name) == 0) { + pfxlen = mask2prefix(ifa->ifa_netmask); + if (ifa->ifa_addr->sa_family == AF_INET) { + if (!pfxlen_v4 || (pfxlen > 0 && pfxlen < pfxlen_v4)) { + if (result4) + memcpy(result4, ifa->ifa_addr, sizeof(struct sockaddr_in)); + if (ifname4) { + strncpy(ifname4, ifa->ifa_name, IFNAMSIZ-1); + ifname4[IFNAMSIZ-1] = '\0'; + } + found_v4 = 1; + pfxlen_v4 = pfxlen > 0 ? pfxlen : 32; + } + } else if (ifa->ifa_addr->sa_family == AF_INET6) { + if (!pfxlen_v6 || (pfxlen > 0 && pfxlen < pfxlen_v6)) { + if (result6) + memcpy(result6, ifa->ifa_addr, sizeof(struct sockaddr_in6)); + if (ifname6) { + strncpy(ifname6, ifa->ifa_name, IFNAMSIZ-1); + ifname6[IFNAMSIZ-1] = '\0'; + } + found_v6 = 1; + pfxlen_v6 = pfxlen > 0 ? pfxlen : 128; + } + } + } + } + } + + /* try to find address on other interfaces */ + if (!found_v4 || !found_v6) { + for (ifa = ifa_head; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL) + continue; + if (ifa->ifa_flags & IFF_LOOPBACK || + !(ifa->ifa_flags & IFF_UP) || + !(ifa->ifa_flags & IFF_RUNNING)) + continue; + if (is_link_local(ifa->ifa_addr)) + continue; + pfxlen = mask2prefix(ifa->ifa_netmask); + if (ifa->ifa_addr->sa_family == AF_INET) { + if (!pfxlen_v4 || (pfxlen > 0 && pfxlen < pfxlen_v4)) { + if (result4) + memcpy(result4, ifa->ifa_addr, sizeof(struct sockaddr_in)); + if (ifname4) { + strncpy(ifname4, ifa->ifa_name, IFNAMSIZ-1); + ifname4[IFNAMSIZ-1] = '\0'; + } + found_v4 = 1; + pfxlen_v4 = pfxlen > 0 ? pfxlen : 32; + } + } else if (ifa->ifa_addr->sa_family == AF_INET6) { + if (!pfxlen_v6 || (pfxlen > 0 && pfxlen < pfxlen_v6)) { + if (result6) + memcpy(result6, ifa->ifa_addr, sizeof(struct sockaddr_in6)); + if (ifname6) { + strncpy(ifname6, ifa->ifa_name, IFNAMSIZ-1); + ifname6[IFNAMSIZ-1] = '\0'; + } + found_v6 = 1; + pfxlen_v6 = pfxlen > 0 ? pfxlen : 128; + } + } + } + } + + freeifaddrs(ifa_head); + + if (found_v4 && found_v6) + return 3; + if (found_v4) + return 1; + if (found_v6) + return 2; + return 0; +} diff --git a/src/ctrl.c b/src/ctrl.c index 303f43b2f..2b23377a9 100644 --- a/src/ctrl.c +++ b/src/ctrl.c @@ -496,7 +496,7 @@ int msg_send(struct dpvs_msg *msg, lcoreid_t cid, uint32_t flags, struct dpvs_ms RTE_LOG(WARNING, MSGMGR, "%s:msg@%p, msg ring of lcore %d quota exceeded\n", __func__, msg, cid); } else if (unlikely(-ENOBUFS == res)) { - RTE_LOG(ERR, MSGMGR, "%s:msg@%p, msg ring of lcore %d is full\n", __func__, msg, res); + RTE_LOG(ERR, MSGMGR, "%s:msg@%p, msg ring of lcore %d is full\n", __func__, msg, cid); add_msg_flags(msg, DPVS_MSG_F_STATE_DROP); rte_atomic16_dec(&msg->refcnt); /* not enqueued, free manually */ return EDPVS_DPDKAPIFAIL; diff --git a/src/global_conf.c b/src/global_conf.c index 9935d8f79..4a28edc79 100644 --- a/src/global_conf.c +++ b/src/global_conf.c @@ -20,6 +20,7 @@ #include "global_conf.h" #include "global_data.h" #include "log.h" +#include "lldp.h" bool g_dpvs_pdump = false; @@ -103,6 +104,13 @@ static int set_log_file(const char *log_file) return EDPVS_OK; } +static void global_defs_handler(vector_t tokens) +{ + // initilize config to default value + g_dpvs_log_tslen = 0; + dpvs_lldp_disable(); +} + static void log_level_handler(vector_t tokens) { char *log_level = set_value(tokens); @@ -189,6 +197,22 @@ static void kni_handler(vector_t tokens) FREE_PTR(str); } +static void lldp_handler(vector_t tokens) +{ + char *str = set_value(tokens); + assert(str); + if (strcasecmp(str, "on") == 0) + dpvs_lldp_enable(); + else if (strcasecmp(str, "off") == 0) + dpvs_lldp_disable(); + else + RTE_LOG(WARNING, CFG_FILE, "invalid lldp config: %s\n", str); + + RTE_LOG(INFO, CFG_FILE, "lldp = %s\n", dpvs_lldp_is_enabled() ? "on" : "off"); + + FREE_PTR(str); +} + #ifdef CONFIG_DPVS_PDUMP static void pdump_handler(vector_t tokens) { @@ -209,13 +233,14 @@ static void pdump_handler(vector_t tokens) void install_global_keywords(void) { - install_keyword_root("global_defs", NULL); + install_keyword_root("global_defs", global_defs_handler); install_keyword("log_level", log_level_handler, KW_TYPE_NORMAL); install_keyword("log_file", log_file_handler, KW_TYPE_NORMAL); install_keyword("log_async_mode", log_async_mode_handler, KW_TYPE_INIT); install_keyword("log_with_timestamp", log_with_timestamp_handler, KW_TYPE_NORMAL); install_keyword("log_async_pool_size", log_async_pool_size_handler, KW_TYPE_INIT); install_keyword("kni", kni_handler, KW_TYPE_INIT); + install_keyword("lldp", lldp_handler, KW_TYPE_NORMAL); #ifdef CONFIG_DPVS_PDUMP install_keyword("pdump", pdump_handler, KW_TYPE_INIT); #endif diff --git a/src/inet.c b/src/inet.c index d5ebcdea1..430babf3c 100644 --- a/src/inet.c +++ b/src/inet.c @@ -28,6 +28,7 @@ #include "icmp.h" #include "icmp6.h" #include "inetaddr.h" +#include "lldp.h" #define INET #define RTE_LOGTYPE_INET RTE_LOGTYPE_USER1 @@ -99,6 +100,8 @@ int inet_init(void) return err; if ((err = inet_addr_init()) != 0) return err; + if ((err = dpvs_lldp_init()) != 0) + return err; return EDPVS_OK; } @@ -107,6 +110,8 @@ int inet_term(void) { int err; + if ((err = dpvs_lldp_term()) != 0) + return err; if ((err = inet_addr_term()) != 0) return err; if ((err = icmpv6_term()) != 0) diff --git a/src/ip_tunnel.c b/src/ip_tunnel.c index 8b6cd9668..3e1e3483b 100644 --- a/src/ip_tunnel.c +++ b/src/ip_tunnel.c @@ -204,6 +204,7 @@ static struct netif_port *tunnel_create(struct ip_tunnel_tab *tab, dev->flag &= ~NETIF_PORT_FLAG_TX_IP_CSUM_OFFLOAD; dev->flag &= ~NETIF_PORT_FLAG_TX_TCP_CSUM_OFFLOAD; dev->flag &= ~NETIF_PORT_FLAG_TX_UDP_CSUM_OFFLOAD; + dev->flag &= ~NETIF_PORT_FLAG_LLDP; err = netif_port_register(dev); if (err != EDPVS_OK) { diff --git a/src/lldp.c b/src/lldp.c new file mode 100644 index 000000000..7edc31a3e --- /dev/null +++ b/src/lldp.c @@ -0,0 +1,1894 @@ +/* + * DPVS is a software load balancer (Virtual Server) based on DPDK. + * + * Copyright (C) 2021 iQIYI (www.iqiyi.com). + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * Jul 2024, yuwenchao@qiyi.com, Initial + */ + +#include +#include +#include "list.h" +#include "timer.h" +#include "ctrl.h" +#include "netif.h" +#include "netif_addr.h" +#include "lldp.h" +#include "conf/lldp.h" + +#define RTE_LOGTYPE_LLDP RTE_LOGTYPE_USER1 + +#define DPVS_LLDP_PDU_MAX 1500 +#define DPVS_LLDP_TTL_DEFAULT 120 +#define DPVS_LLDP_TX_INTERVAL 30 +#define DPVS_LLDP_UPDATE_INTERVAL 600 + +#define DPVS_LLDP_TL_TYPE(tl) ((rte_be_to_cpu_16(tl) & 0xfe00) >> 9) +#define DPVS_LLDP_TL_LEN(tl) ((rte_be_to_cpu_16(tl) & 0x01ff)) +#define DPVS_LLDP_TL(type, len) (rte_cpu_to_be_16((((type) & 0x7f) << 9) | ((len) & 0x1ff))) + +#define lldp_type_equal(t1, t2) (((t1).type == (t2).type) && ((t1).subtype == (t2).subtype)) + +/* helper macro used in lldp_type_ops::dump + * @buf: target string buffer, must be an array + * @pos: start position for this snprintf, must be an initialized integer variable + * */ +#define lldp_dump_snprintf(buf, pos, fmt, ...) \ + do { \ + int res = snprintf(&(buf)[pos], sizeof(buf) - pos, fmt, ##__VA_ARGS__); \ + if (unlikely(res < 0)) \ + return EDPVS_IO; \ + (pos) += res; \ + if ((pos) >= sizeof(buf)) \ + return EDPVS_NOROOM; \ + } while (0) + +/* helper macro used ihn lldp_type_ops::dump + * @buf: target string buffer, must be an array + * @pos: start position for this snprintf, must be an initialized integer variable + * @s: non-null-terminated string (use lldp_dump_snprintf for null-terminated string) + * @n: length of s + * @ends: ending string appended into buf + * */ +#define lldp_dump_strcpy(buf, pos, s, n, ends) \ + do { \ + int i, endslen = strlen(ends); \ + if (unlikely((endslen + (n)) >= (sizeof(buf) - (pos)))) \ + return EDPVS_NOROOM; \ + rte_memcpy(&(buf)[pos], s, n); \ + (pos) += (n); \ + for (i = 0; i < endslen; i++) \ + (buf)[(pos)++] = ends[i]; \ + (buf)[pos] = '\0'; \ + } while (0) + +const struct rte_ether_addr LLDP_ETHER_ADDR_DST = { + .addr_bytes = {0x01, 0x80, 0xC2, 0x00, 0x00, 0x0E} +}; + +/* + * LLDP is processed only on master lcore, all data structures are free of lock + */ + +typedef struct { + uint8_t type; + uint32_t subtype; +} lldp_type_t; + +struct lldp_port { + struct netif_port *dev; + struct list_head head; /* lldp_entry list head, sorted by lldp type */ + struct list_head node; + struct dpvs_timer timer; + uint32_t timeout; + uint16_t entries; + uint16_t neigh; /* DPVS_LLDP_NODE_xxx */ +}; + +struct lldp_entry { + struct list_head node; + struct lldp_port *port; + uint8_t stale; + lldp_type_t type; + uint16_t len; /* host endian */ + + /* lldp pdu */ + uint16_t typelen; /* network endian */ + char value[0]; +}; + +struct lldp_type_ops { + uint8_t type; + + /* + * Parse LLDP type and subtype from LLDP PDU + * @params + * llpdu: lldp pdu + * type: where to store the parsed type id, must not be NULL + * len: where to store the parse data len for the type, can be NULL + * @return + * DPVS error code num + * */ + int (*parse_type)(const char *llpdu, lldp_type_t *type, uint16_t *len); + + /* + * Generate LLDP PDU, and store it to lldpdu + * @params + * dev: physical netif port + * subtype: subtype of the LLDP PDU + * lldpdu: lldp pdu buffer + * len: buffer size + * @return + * the lldp pdu length on success or buffer not big enough + * dpvs negative error code on error + * */ + int (*local_lldp)(const struct netif_port *dev, uint32_t subtype, char *buf, size_t len); + + /* + * Translate LLDP PDU, store the translated message into buf. + * @params + * e: lldp entry + * buf: message buffer + * len: message buffer size + * @return (similar to "snprintf") + * the message length on success or buffer not big enough + * negative error code on error + * the returned message always teminates with '\0'. + * */ + int (*dump)(const struct lldp_entry *e, char *buf, size_t len); + + /* + * Actions to take after lldp pdu changed (add, update) + * @params: + * entry: the newly added entry + * @return + * dpvs error code + * */ + int (*on_change)(const struct lldp_entry *entry); +}; + +static int lldp_enable = 0; +static struct dpvs_timer lldp_xmit_timer; +static struct dpvs_timer lldp_update_timer; + +static char lldp_sn[256]; +static struct utsname lldp_uname; + +static struct list_head lldp_ports[DPVS_LLDP_NODE_MAX]; +static struct lldp_type_ops *lldp_types[DPVS_LLDP_TYPE_MAX] = { NULL }; + +static int lldp_xmit_start(void); +static int lldp_xmit_stop(void); + +void dpvs_lldp_enable(void) +{ + int err; + + if (lldp_enable) + return; + + if (dpvs_state_get() == DPVS_STATE_NORMAL) { + if ((err = lldp_xmit_start()) != EDPVS_OK) { + RTE_LOG(ERR, LLDP, "%s: fail to enable lldp -- %s\n", + __func__, dpvs_strerror(err)); + return; + } + } + + lldp_enable = 1; +} + +void dpvs_lldp_disable(void) +{ + int err; + + if (!lldp_enable) + return; + + if (dpvs_state_get() == DPVS_STATE_NORMAL) { + if ((err = lldp_xmit_stop()) != EDPVS_OK) { + RTE_LOG(ERR, LLDP, "%s: fail to disable lldp -- %s\n", + __func__, dpvs_strerror(err)); + return; + } + } + + lldp_enable = 0; +} + +bool dpvs_lldp_is_enabled(void) +{ + return !!lldp_enable; +} + +static int lldp_serail_number_init(void) +{ + FILE *fp; + char *ptr; + + fp = fopen("/sys/class/dmi/id/product_serial", "r"); + if (!fp) { + RTE_LOG(WARNING, LLDP, "%s: fail to open serial number file\n", __func__); + snprintf(lldp_sn, sizeof(lldp_sn), "%s", "Unknown"); + return EDPVS_SYSCALL; + } + + if (!fgets(lldp_sn, sizeof(lldp_sn), fp)) { + RTE_LOG(WARNING, LLDP, "%s: fail to read serial number file\n", __func__); + snprintf(lldp_sn, sizeof(lldp_sn), "%s", "Unknown"); + return EDPVS_IO; + } + + /* remove the tailing LF character */ + ptr = strrchr(lldp_sn, '\n'); + if (ptr) + *ptr = '\0'; + + return EDPVS_OK; +} + +static inline int lldp_type_cmp(lldp_type_t *t1, lldp_type_t *t2) +{ + if (t1->type < t2->type) + return -1; + if (t1->type > t2->type) + return 1; + if (t1->subtype < t2->subtype) + return -1; + if (t1->subtype > t2->subtype) + return 1; + return 0; +} + +static int lldp_type_register(struct lldp_type_ops *ops) +{ + if (!ops || ops->type >= DPVS_LLDP_TYPE_MAX) + return EDPVS_INVAL; + + if (lldp_types[ops->type] != NULL) + return EDPVS_EXIST; + + if (!ops->parse_type || !ops->dump) + return EDPVS_INVAL; + + lldp_types[ops->type] = ops; + return EDPVS_OK; +} + +static int lldp_type_unregister(struct lldp_type_ops *ops) +{ + if (!ops || ops->type >= DPVS_LLDP_TYPE_MAX) + return EDPVS_INVAL; + + if (!lldp_types[ops->type]) + return EDPVS_NOTEXIST; + + lldp_types[ops->type] = NULL; + return EDPVS_OK; +} + +static struct lldp_type_ops *lldp_type_get(lldp_type_t type) +{ + if (type.type >= DPVS_LLDP_TYPE_MAX) + return NULL; + return lldp_types[type.type]; +} + +static int lldp_parse_type_default(const char *lldpdu, lldp_type_t *type, uint16_t *len) +{ + assert(NULL != type); + + type->type = DPVS_LLDP_TL_TYPE((uint16_t)(*lldpdu)); + type->subtype = 0; + if (!LLDP_TYPE_VALID(type->type)) { + type->type = 0; + return EDPVS_INVAL; + } + if (len) + *len = DPVS_LLDP_TL_LEN(*((uint16_t *)lldpdu)); + + return EDPVS_OK; +} + +static int lldp_local_pdu_end(const struct netif_port *dev, uint32_t subtype, char *buf, size_t len) +{ + uint16_t *typelen = (uint16_t *)buf; + + if (len >= 2) + *typelen = DPVS_LLDP_TL(LLDP_TYPE_END, 0); + else + memset(buf, 0, len); + return 2; +} + +static int lldp_dump_end(const struct lldp_entry *e, char *buf, size_t len) +{ + return snprintf(buf, len, "%s\n", "End of LLDPDU TLV"); +} + +static int lldp_parse_type_chassis_id(const char *lldpdu, lldp_type_t *type, uint16_t *len) +{ + assert(type != NULL); + + type->type = DPVS_LLDP_TL_TYPE((uint16_t)(*lldpdu)); + if (!LLDP_TYPE_VALID(type->type)) { + type->type = 0; + return EDPVS_INVAL; + } + + type->subtype = *(lldpdu + 2); + if (!LLDP_CHASSIS_ID_VALID(type->subtype)) { + type->subtype = 0; + return EDPVS_INVAL; + } + + if (len) + *len = DPVS_LLDP_TL_LEN(*((uint16_t *)lldpdu)); + + return EDPVS_OK; +} + +static int lldp_local_pdu_chassis_id(const struct netif_port *dev, uint32_t subtype, char *buf, size_t len) +{ + if (len >= 2 + 7) { + *((uint16_t *)buf) = DPVS_LLDP_TL(LLDP_TYPE_CHASSIS_ID, 7); + buf[2] = LLDP_CHASSIS_ID_MAC_ADDRESS; + rte_memcpy(&buf[3], &dev->addr, 6); + } else { + memset(buf, 0, len); + } + return 2 + 7; +} + +static int lldp_dump_chassis_id(const struct lldp_entry *e, char *buf, size_t len) +{ + const uint8_t *ptr = (const uint8_t *)e->value; /* Chassis ID Type */ + int pos = 0; + char tbuf[512], ipbuf[64]; + + lldp_dump_snprintf(tbuf, pos, "%s (%d)\n", "Chassis ID TLV", e->type.type); + + assert(e->type.subtype == *ptr); + ++ptr; /* Chassis ID Data */ + switch (e->type.subtype) { + case LLDP_CHASSIS_ID_CHASSIS_COMPONENT: + lldp_dump_snprintf(tbuf, pos, "%s", "\tChassis Component: "); + lldp_dump_strcpy(tbuf, pos, ptr, e->len - 1, "\n"); + break; + case LLDP_CHASSIS_ID_INTERFACE_ALIAS: + lldp_dump_snprintf(tbuf, pos, "%s", "\tInterface Alias: "); + lldp_dump_strcpy(tbuf, pos, ptr, e->len - 1, "\n"); + break; + case LLDP_CHASSIS_ID_PORT_COMPONENT: + lldp_dump_snprintf(tbuf, pos, "%s", "\tPort Component: "); + lldp_dump_strcpy(tbuf, pos, ptr, e->len - 1, "\n"); + break; + case LLDP_CHASSIS_ID_MAC_ADDRESS: + if (unlikely(e->len < 7)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tMAC: %02x:%02x:%02x:%02x:%02x:%02x\n", + ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]); + break; + case LLDP_CHASSIS_ID_NETWORK_ADDRESS: + switch (*ptr) { + case LLDP_ADDR_IPV4: + if (unlikely(e->len < 6)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tIPv4: %s\n", inet_ntop(AF_INET, ptr + 1, + ipbuf, sizeof(ipbuf)) ?: "Unknown"); + break; + case LLDP_ADDR_IPV6: + if (unlikely(e->len < 18)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tIPv6: %s\n", inet_ntop(AF_INET6, ptr + 1, + ipbuf, sizeof(ipbuf)) ?: "Unknown"); + break; + default: + if (unlikely(e->len <= 2)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tNetwork Address Type %d:", *ptr); + pos += binary2hexstr(ptr + 1, e->len - 2, &tbuf[pos], sizeof(tbuf) - pos); + if (unlikely(pos >= sizeof(tbuf))) + return EDPVS_NOROOM; + lldp_dump_snprintf(tbuf, pos, "%c", '\n'); + break; + } + break; + case LLDP_CHASSIS_ID_INTERFACE_NAME: + lldp_dump_snprintf(tbuf, pos, "%s", "\tInterface Name: "); + lldp_dump_strcpy(tbuf, pos, ptr, e->len - 1, "\n"); + break; + case LLDP_CHASSIS_ID_LOCALLY_ASSIGNED: + lldp_dump_snprintf(tbuf, pos, "%s", "\tLocal: "); + lldp_dump_strcpy(tbuf, pos, ptr, e->len - 1, "\n"); + break; + default: + lldp_dump_snprintf(tbuf, pos, "\t%s: ", "Bad Chassis ID"); + pos += binary2print(ptr, e->len - 1, &tbuf[pos], sizeof(tbuf) - pos); + if (unlikely(pos >= sizeof(tbuf))) + return EDPVS_NOROOM; + lldp_dump_snprintf(tbuf, pos, "%c", '\n'); + break; + } + + if (pos >= len) { + rte_memcpy(buf, tbuf, len - 1); + buf[len - 1] = '\0'; + } else { + rte_memcpy(buf, tbuf, pos); + buf[pos] = '\0'; + } + return pos; +} + +static int lldp_parse_type_port_id(const char *lldpdu, lldp_type_t *type, uint16_t *len) +{ + assert(type != NULL); + + type->type = DPVS_LLDP_TL_TYPE((uint16_t)(*lldpdu)); + if (!LLDP_TYPE_VALID(type->type)) { + type->type = 0; + return EDPVS_INVAL; + } + + type->subtype = *(lldpdu + 2); + if (!LLDP_PORT_ID_VALID(type->subtype)) { + type->subtype = 0; + return EDPVS_INVAL; + } + + if (len) + *len = DPVS_LLDP_TL_LEN(*((uint16_t *)lldpdu)); + + return EDPVS_OK; +} + +static int lldp_local_pdu_port_id(const struct netif_port *dev, uint32_t subtype, char *buf, size_t len) +{ + size_t datalen = strlen(dev->name); + + assert(datalen < IFNAMSIZ); + + if (len >= 2 + 1 + datalen) { + *((uint16_t *)buf) = DPVS_LLDP_TL(LLDP_TYPE_PORT_ID, 1 + datalen); + buf[2] = LLDP_PORT_ID_INTERFACE_NAME; + rte_memcpy(&buf[3], &dev->name, datalen); + } else { + memset(buf, 0, len); + } + + return 2 + 1 + datalen; +} + +static int lldp_dump_port_id(const struct lldp_entry *e, char *buf, size_t len) +{ + const uint8_t *ptr = (const uint8_t *)e->value; /* Port ID Subtype */ + int pos = 0; + char tbuf[512], ipbuf[64]; + + lldp_dump_snprintf(tbuf, pos, "%s (%d)\n", "Port ID TLV", e->type.type); + assert(e->type.subtype == *ptr); + + ++ptr; /* Port ID Data */ + switch (e->type.subtype) { + case LLDP_PORT_ID_INTERFACE_ALIAS: + lldp_dump_snprintf(tbuf, pos, "%s", "\tInterface Alias: "); + lldp_dump_strcpy(tbuf, pos, ptr, e->len - 1, "\n"); + break; + case LLDP_PORT_ID_PORT_COMPONENT: + lldp_dump_snprintf(tbuf, pos, "%s", "\tPort Component: "); + lldp_dump_strcpy(tbuf, pos, ptr, e->len - 1, "\n"); + break; + case LLDP_PORT_ID_MAC_ADDRESS: + if (unlikely(e->len < 7)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tMAC: %02x:%02x:%02x:%02x:%02x:%02x\n", + ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]); + break; + case LLDP_PORT_ID_NETWORK_ADDRESS: + switch (*ptr) { + case LLDP_ADDR_IPV4: + if (unlikely(e->len < 6)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tIPv4: %s\n", inet_ntop(AF_INET, ptr + 1, + ipbuf, sizeof(ipbuf)) ?: "Unknown"); + break; + case LLDP_ADDR_IPV6: + if (unlikely(e->len < 18)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tIPv6: %s\n", inet_ntop(AF_INET6, ptr + 1, + ipbuf, sizeof(ipbuf)) ?: "Unknown"); + break; + default: + if (unlikely(e->len <= 2)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tNetwork Address Type %d:", *ptr); + pos += binary2hexstr(ptr + 1, e->len - 2, &tbuf[pos], sizeof(tbuf) - pos); + if (unlikely(pos >= sizeof(tbuf))) + return EDPVS_NOROOM; + lldp_dump_snprintf(tbuf, pos, "%c", '\n'); + break; + } + break; + case LLDP_PORT_ID_INTERFACE_NAME: + lldp_dump_snprintf(tbuf, pos, "%s", "\tInterface Name: "); + lldp_dump_strcpy(tbuf, pos, ptr, e->len - 1, "\n"); + break; + case LLDP_PORT_ID_AGENT_CIRCUIT_ID: + lldp_dump_snprintf(tbuf, pos, "\t%s: ", "Agent Circuit ID"); + pos += binary2hexstr(ptr, e->len - 1, &tbuf[pos], sizeof(tbuf) - pos); + if (unlikely(pos >= sizeof(tbuf))) + return EDPVS_NOROOM; + lldp_dump_snprintf(tbuf, pos, "%c", '\n'); + break; + case LLDP_PORT_ID_LOCALLY_ASSIGNED: + lldp_dump_snprintf(tbuf, pos, "%s", "\tLocal: "); + lldp_dump_strcpy(tbuf, pos, ptr, e->len -1, "\n"); + break; + default: + lldp_dump_snprintf(tbuf, pos, "\t%s: ", "Bad Port ID"); + pos += binary2print(ptr, e->len - 1, &tbuf[pos], sizeof(tbuf) - pos); + if (unlikely(pos >= sizeof(tbuf))) + return EDPVS_NOROOM; + lldp_dump_snprintf(tbuf, pos, "%c", '\n'); + break; + } + + if (pos >= len) { + rte_memcpy(buf, tbuf, len - 1); + buf[len - 1] = '\0'; + } else { + rte_memcpy(buf, tbuf, pos); + buf[pos] = '\0'; + } + return pos; +} + +static int lldp_local_pdu_ttl(const struct netif_port *dev, uint32_t subtype, char *buf, size_t len) +{ + uint16_t *data; + + if (len >= 4) { + data = (uint16_t *)buf; + *data++ = DPVS_LLDP_TL(LLDP_TYPE_TTL, 2); + *data = rte_cpu_to_be_16(DPVS_LLDP_TTL_DEFAULT); + } else { + memset(buf, 0, len); + } + + return 4; +} + +static int lldp_dump_ttl(const struct lldp_entry *e, char *buf, size_t len) +{ + uint16_t *ttl = (uint16_t *)e->value; + return snprintf(buf, len, "Time to Live TLV (%d)\n\t%d\n", e->type.type, rte_be_to_cpu_16(*ttl)); +} + +static int lldp_on_change_ttl(const struct lldp_entry *e) +{ + struct lldp_port *port = e->port; + uint16_t ttl; + + /* Lifespan of local lldp caches is not decided by ttl. Actually, they are + * updated periodically in every DPVS_LLDP_UPDATE_INTERVAL second. If not updated + * in 3 * DPVS_LLDP_UPDATE_INTERVAL seconds, they are expired and removed. + * */ + if (port->neigh == DPVS_LLDP_NODE_LOCAL) + return EDPVS_OK; + + ttl = rte_be_to_cpu_16(*((uint16_t *)e->value)); + if (ttl != port->timeout) { + RTE_LOG(INFO, LLDP, "%s: update neigh lldp ttl %u -> %u\n", __func__, port->timeout, ttl); + port->timeout = ttl; + } + + return EDPVS_OK; +} + +static int lldp_local_pdu_port_desc(const struct netif_port *dev, uint32_t subtype, char *buf, size_t len) +{ + size_t desc_len; + char desc[128]; + + desc_len = snprintf(desc, sizeof(desc), "DPVS Server Port: Interface %s, Index %d, Kni %s", + dev->name, dev->id, dev->kni.kni ? dev->kni.name : "None"); + if (2 + desc_len <= len) { + *((uint16_t *)buf) = DPVS_LLDP_TL(LLDP_TYPE_PORT_DESC, desc_len); + rte_memcpy(&buf[2], desc, desc_len); + } else { + memset(buf, 0, len); + } + + return 2 + desc_len; +} + +static int lldp_dump_port_desc(const struct lldp_entry *e, char *buf, size_t len) +{ + int pos = 0; + char tbuf[1024]; + + lldp_dump_snprintf(tbuf, pos, "Port Description TLV (%d)\n\t", e->type.type); + if (likely(e->len > 0)) + lldp_dump_strcpy(tbuf, pos, e->value, e->len, "\n"); + + if (pos >= len) { + rte_memcpy(buf, tbuf, len - 1); + buf[len - 1] = '\0'; + } else { + rte_memcpy(buf, tbuf, pos); + buf[pos] = '\0'; + } + + return pos; +} + +static int lldp_local_pdu_sys_name(const struct netif_port *dev, uint32_t subtype, char *buf, size_t len) +{ + size_t host_len; + char hostname[HOST_NAME_MAX + 1]; + + if (unlikely(gethostname(hostname, sizeof(hostname)) != 0)) + snprintf(hostname, sizeof(hostname), "%s", "Unknown"); + + host_len = strlen(hostname); + if (2 + host_len <= len) { + *((uint16_t *)buf) = DPVS_LLDP_TL(LLDP_TYPE_SYS_NAME, host_len); + rte_memcpy(&buf[2], hostname, host_len); + } else { + memset(buf, 0, len); + } + + return 2 + host_len; +} + +static int lldp_dump_sys_name(const struct lldp_entry *e, char *buf, size_t len) +{ + int pos = 0; + char tbuf[1024]; + + lldp_dump_snprintf(tbuf, pos, "System Name TLV (%d)\n\t", e->type.type); + if (likely(e->len > 0)) + lldp_dump_strcpy(tbuf, pos, e->value, e->len, "\n"); + + if (pos >= len) { + rte_memcpy(buf, tbuf, len - 1); + buf[len - 1] = '\0'; + } else { + rte_memcpy(buf, tbuf, pos); + buf[pos] = '\0'; + } + + return pos; +} + +static int lldp_local_pdu_sys_desc(const struct netif_port *dev, uint32_t subtype, char *buf, size_t len) +{ + int rc; + + rc = snprintf(buf + 2, len - 2, "%s %s %s %s %s, Serail Number %s", + lldp_uname.sysname, lldp_uname.nodename, lldp_uname.release, + lldp_uname.version, lldp_uname.machine, lldp_sn); + if (unlikely(rc < 0)) + return EDPVS_IO; + *((uint16_t *)buf) = DPVS_LLDP_TL(LLDP_TYPE_SYS_DESC, rc); + + return rc; +} + +static int lldp_dump_sys_desc(const struct lldp_entry *e, char *buf, size_t len) +{ + int pos = 0; + char tbuf[1024]; + + lldp_dump_snprintf(tbuf, pos, "System Description TLV (%d)\n\t", e->type.type); + if (likely(e->len > 0)) + lldp_dump_strcpy(tbuf, pos, e->value, e->len, "\n"); + + if (pos >= len) { + rte_memcpy(buf, tbuf, len - 1); + buf[len - 1] = '\0'; + } else { + rte_memcpy(buf, tbuf, pos); + buf[pos] = '\0'; + } + return pos; +} + +static const char *lldp_bit2sys_cap(uint16_t capacities, uint8_t bitpos) +{ + switch (capacities & (1UL << bitpos)) { + case 0x0001: + return "Other"; + case 0x0002: + return "Repeater"; + case 0x0004: + return "Bridge"; + case 0x0008: + return "WLAN Access Point"; + case 0x0010: + return "Router"; + case 0x0020: + return "Telephone"; + case 0x0040: + return "DOCSIS cable device"; + case 0x0080: + return "Station Only"; + case 0x0100: + return "Client"; + case 0x0200: + return "ISDN Terminal Adapter"; + case 0x0400: + return "Cryptographic Device"; + case 0x0800: + return "Voice Gateway"; + case 0x1000: + return "LAN Endpoint"; + case 0x2000: + case 0x4000: + case 0x8000: + return "Reserved"; + default: + return ""; + } + return ""; +} + +static int lldp_local_pdu_sys_cap(const struct netif_port *dev, uint32_t subtype, char *buf, size_t len) +{ + if (len >= 2 + 4) { + *((uint16_t *)&buf[0]) = DPVS_LLDP_TL(LLDP_TYPE_SYS_CAP, 4); + *((uint16_t *)&buf[2]) = rte_cpu_to_be_16(0x80); /* Capacity: Station Only */ + *((uint16_t *)&buf[4]) = rte_cpu_to_be_16(0x80); /* Enabled: Station Only */ + } + + return 2 + 4; +} + +static int lldp_dump_sys_cap(const struct lldp_entry *e, char *buf, size_t len) +{ + uint8_t i, first; + uint16_t capacities, enables; + int pos = 0; + char tbuf[256]; + + if (e->len != 4) + return EDPVS_INVPKT; + capacities = rte_be_to_cpu_16(*((uint16_t *)&e->value[0])); + enables = rte_be_to_cpu_16(*((uint16_t *)&e->value[2])); + + lldp_dump_snprintf(tbuf, pos, "System Capabilities TLV (%d)\n", e->type.type); + + first = 1; + for (i = 0; i < 16; i++) { + if (!(capacities & (1UL << i))) + continue; + if (first) { + lldp_dump_snprintf(tbuf, pos, "\tSystem capabilities: %s", + lldp_bit2sys_cap(capacities, i)); + first = 0; + } else { + lldp_dump_snprintf(tbuf, pos, ", %s", lldp_bit2sys_cap(capacities, i)); + } + } + lldp_dump_snprintf(tbuf, pos, "%c", '\n'); + + first = 1; + for (i = 0; i < 16; i++) { + if (!(enables & (1UL << i))) + continue; + if (first) { + lldp_dump_snprintf(tbuf, pos, "\tEnabled capabilities: %s", + lldp_bit2sys_cap(enables, i)); + first = 0; + } else { + lldp_dump_snprintf(tbuf, pos, ", %s", lldp_bit2sys_cap(enables, i)); + } + } + lldp_dump_snprintf(tbuf, pos, "%c", '\n'); + + if (pos >= len) { + rte_memcpy(buf, tbuf, len - 1); + buf[len - 11] = '\0'; + } else { + rte_memcpy(buf, tbuf, pos); + buf[pos] = '\0'; + } + return pos; +} + +static int lldp_parse_type_mng_addr(const char *lldpdu, lldp_type_t *type, uint16_t *len) +{ + assert(NULL != type); + + type->type = DPVS_LLDP_TL_TYPE((uint16_t)(*lldpdu)); + if (!LLDP_TYPE_VALID(type->type)) { + type->type = 0; + return EDPVS_INVAL; + } + type->subtype = *((uint8_t *)(lldpdu + 3)); + + if (len) + *len = DPVS_LLDP_TL_LEN(*((uint16_t *)lldpdu)); + + return EDPVS_OK; +} + +static int lldp_local_pdu_mng_addr(const struct netif_port *dev, uint32_t subtype, char *buf, size_t len) +{ + int rc; + uint8_t tbuf[512]; + uint8_t *ptr; + struct sockaddr_storage addr; + char ifname[IFNAMSIZ]; + + ptr = tbuf + 2; + *(ptr + 1) = subtype; + switch (subtype) { + case LLDP_ADDR_ALL802: + *ptr = 7; + rte_memcpy(ptr + 2, &dev->addr, 6); + ptr += 8; + break; + case LLDP_ADDR_IPV4: + *ptr = 5; + rc = get_host_addr(dev->kni.kni ? dev->kni.name : NULL, &addr, NULL, ifname, NULL); + if (rc < 0) + return rc; + if (rc & 0x1) + rte_memcpy(ptr + 2, &((struct sockaddr_in *)&addr)->sin_addr.s_addr, 4); + else + ifname[0] = '\0'; + ptr += 6; + break; + case LLDP_ADDR_IPV6: + *ptr = 17; + rc = get_host_addr(dev->kni.kni ? dev->kni.name : NULL, NULL, &addr, NULL, ifname); + if (rc < 0) + return rc; + if (rc &0x2) + rte_memcpy(ptr + 2, &((struct sockaddr_in6 *)&addr)->sin6_addr, 16); + else + ifname[0] = '\0'; + ptr += 18; + break; + default: + return EDPVS_NOTSUPP; + } + + if (subtype == LLDP_ADDR_ALL802) { + *ptr++ = 2; /* Interface Subtype: Ifindex */ + *((uint32_t *)ptr) = rte_cpu_to_be_32(dev->id); + } else if (ifname[0]) { + *ptr++ = 2; /* Interface Subtype: Ifindex */ + rc = linux_ifname2index(ifname); + if (rc < 0) + return EDPVS_SYSCALL; + *((uint32_t *)ptr) = rte_cpu_to_be_32(rc); + } else { + *ptr++ = 1; /* Interface Subtype: Unknown */ + *((uint32_t *)ptr) = 0; + } + + ptr += 4; /* OID String Length */ + *ptr++ = 0; + + *((uint16_t *)tbuf) = DPVS_LLDP_TL(LLDP_TYPE_MNG_ADDR, ptr - tbuf - 2); + + if (ptr - tbuf > len) + rte_memcpy(buf, tbuf, len); + else + rte_memcpy(buf, tbuf, ptr - tbuf); + return ptr - tbuf; +} + +static int lldp_dump_mng_addr(const struct lldp_entry *e, char *buf, size_t len) +{ + const uint8_t *ptr = (const uint8_t *)e->value; /* Address Length */ + uint8_t addrlen, intf_subtype, oidlen; + int pos = 0; + char tbuf[1024], ipbuf[64]; + + lldp_dump_snprintf(tbuf, pos, "%s (%d)\n", "Management Address TLV", e->type.type); + addrlen = *ptr; + ++ptr; /* Address Subtype */ + assert(e->type.subtype == *ptr); + + ++ptr; /* Management Address */ + switch (e->type.subtype) { + case LLDP_ADDR_ALL802: + if (unlikely(addrlen < 7)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tMAC: %02x:%02x:%02x:%02x:%02x:%02x\n", + ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]); + break; + case LLDP_ADDR_IPV4: + if (unlikely(addrlen < 5)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tIPv4: %s\n", + inet_ntop(AF_INET, ptr, ipbuf, sizeof(ipbuf)) ?: "Unknown"); + break; + case LLDP_ADDR_IPV6: + if (unlikely(addrlen < 17)) + return EDPVS_INVPKT; + lldp_dump_snprintf(tbuf, pos, "\tIPv6: %s\n", + inet_ntop(AF_INET6, ptr, ipbuf, sizeof(ipbuf)) ?: "Unknown"); + break; + default: + lldp_dump_snprintf(tbuf, pos, "\tNetwork Address Type(%d): ", e->type.subtype); + pos += binary2hexstr(ptr, addrlen - 1, &tbuf[pos], sizeof(tbuf) - pos); + if (unlikely(pos >= sizeof(tbuf))) + return EDPVS_NOROOM; + lldp_dump_snprintf(tbuf, pos, "%c", '\n'); + break; + } + + ptr = ptr + addrlen - 1; /* Interface Subtype */ + intf_subtype = *ptr; + switch (intf_subtype) { + case 1: + lldp_dump_snprintf(tbuf, pos, "\tUnknown interface subtype(%d): ", intf_subtype); + break; + case 2: + lldp_dump_snprintf(tbuf, pos, "%s", "\tIfindex: "); + break; + case 3: + lldp_dump_snprintf(tbuf, pos, "%s", "\tSystem port number: "); + break; + default: + lldp_dump_snprintf(tbuf, pos, "\tUnsupported interface subtype(%d): ", intf_subtype); + break; + } + ++ptr; /* Interface */ + lldp_dump_snprintf(tbuf, pos, "%d\n", rte_be_to_cpu_32(*((uint32_t *)ptr))); + + ptr += 4; /* OID String Length */ + oidlen = *ptr; + + ++ptr; /* OID String */ + if (oidlen > 128) + lldp_dump_snprintf(tbuf, pos, "\tOID: Invalid length = %d\n", oidlen); + else if (oidlen > 0) { + lldp_dump_snprintf(tbuf, pos, "%s", "\tOID: "); + pos += binary2hexstr((const uint8_t *)ptr, oidlen, &tbuf[pos], sizeof(tbuf) - pos); + if (pos >= sizeof(tbuf)) + return EDPVS_NOROOM; + lldp_dump_snprintf(tbuf, pos, "%c", '\n'); + } + + if (pos >= len) { + rte_memcpy(buf, tbuf, len - 1); + buf[len - 1] = '\0'; + } else { + rte_memcpy(buf, tbuf, pos); + buf[pos] = '\0'; + } + return pos; +} + +static int lldp_parse_type_org(const char *lldpdu, lldp_type_t *type, uint16_t *len) +{ + assert(type != NULL); + + type->type = DPVS_LLDP_TL_TYPE((uint16_t)(*lldpdu)); + if (!LLDP_TYPE_VALID(type->type)) { + type->type = 0; + return EDPVS_INVAL; + } + + /* subtype := ((24-bit Orgnization Unique Code) << 8) | (8-bit Subtype) */ + type->subtype = rte_be_to_cpu_32(*((uint32_t *)&lldpdu[2])); + + if (len) + *len = DPVS_LLDP_TL_LEN(*((uint16_t *)lldpdu)); + + return EDPVS_OK; +} + +static int lldp_dump_org_specific(const struct lldp_entry *e, char *buf, size_t len) +{ + // TODO: Implement Organizationally Specific TLVs + + const unsigned char *ptr = (unsigned char *)e->value; + int pos = 0; + char tbuf[1024]; + + if (e->len < 4) + return EDPVS_INVPKT; + + lldp_dump_snprintf(tbuf, pos, "Organizationally Specific TLV (%d): Code %02x:%02x:%02x, " + "Subtype %02d\n\t", e->type.type, ptr[0], ptr[1], ptr[2], ptr[3]); + pos += binary2hexstr((const uint8_t *)(&ptr[4]), e->len - 4, &tbuf[pos], sizeof(tbuf) - pos); + if (pos >= sizeof(tbuf)) + return EDPVS_NOROOM; + lldp_dump_snprintf(tbuf, pos, "%c", '\n'); + + if (pos >= len) { + rte_memcpy(buf, tbuf, len - 1); + buf[len - 1] = '\0'; + } else { + rte_memcpy(buf, tbuf, pos); + buf[pos] = '\0'; + } + return pos; +} + +static struct lldp_port *lldp_port_get(portid_t pid, uint16_t neigh) +{ + struct lldp_port *lp; + + if (unlikely(neigh >= DPVS_LLDP_NODE_MAX)) + return NULL; + + list_for_each_entry(lp, &lldp_ports[neigh], node) { + if (lp->dev->id == pid) { + assert(lp->neigh == neigh); + return lp; + } + } + return NULL; +} + +static void lldp_port_hash(struct lldp_port *port) +{ + struct lldp_port *entry, *next = NULL; + + assert(port->neigh < DPVS_LLDP_NODE_MAX); + + list_for_each_entry(entry, &lldp_ports[port->neigh], node) { + if (entry->dev->id >= port->dev->id) { + next = entry; + break; + } + } + + if (NULL != next) + list_add_tail(&port->node, &next->node); + else + list_add_tail(&port->node, &lldp_ports[port->neigh]); +} + +static inline void lldp_port_unhash(struct lldp_port *port) +{ + list_del_init(&port->node); +} + +static int lldp_entry_del(struct lldp_entry *entry); +static int lldp_port_del(struct lldp_port *port, bool in_timer) +{ + int err; + struct lldp_entry *entry, *next; + + lldp_port_unhash(port); + + list_for_each_entry_safe(entry, next, &port->head, node) { + err = lldp_entry_del(entry); + if (err != EDPVS_OK) + RTE_LOG(WARNING, LLDP, "%s: fail to del lldp %s entry, port %s type %d:%d error %s\n", + __func__, port->neigh ? "neigh" : "local", port->dev->name, + entry->type.type, entry->type.subtype, dpvs_strerror(err)); + } + assert(port->entries == 0); + + if (in_timer) + err = dpvs_timer_cancel_nolock(&port->timer, true); + else + err = dpvs_timer_cancel(&port->timer, true); + if (err != EDPVS_OK) + RTE_LOG(WARNING, LLDP, "%s: fail to cancel lldp port timer, port %s error %s\n", + __func__, port->dev->name, dpvs_strerror(err)); + + rte_free(port); + return EDPVS_OK; +} + +static int lldp_port_timeout(void *arg) +{ + struct lldp_port *port = arg; + + RTE_LOG(DEBUG, LLDP,"%s: %s lldp cache on %s expired\n", __func__, + port->neigh == DPVS_LLDP_NODE_LOCAL ? "local" : "neighbor", + port->dev->name); + + lldp_port_del(port, true); + return DTIMER_STOP; +} + +static int lldp_port_add(struct netif_port *dev, uint16_t neigh, uint16_t timeout, bool in_timer) +{ + int err; + struct lldp_port *lp; + struct timeval to = { .tv_sec = timeout }; + + if (neigh >= DPVS_LLDP_NODE_MAX) + return EDPVS_INVAL; + + if (lldp_port_get(dev->id, neigh)) + return EDPVS_EXIST; + + lp = rte_zmalloc("lldp_port", sizeof(*lp), RTE_CACHE_LINE_SIZE); + if (unlikely(!lp)) + return EDPVS_NOMEM; + + lp->dev = dev; + lp->neigh = neigh; + lp->timeout = timeout ?: DPVS_LLDP_TTL_DEFAULT; + INIT_LIST_HEAD(&lp->head); + + lldp_port_hash(lp); + + dpvs_time_rand_delay(&to, 1000000); + if (in_timer) + err = dpvs_timer_sched_nolock(&lp->timer, &to, lldp_port_timeout, lp, true); + else + err = dpvs_timer_sched(&lp->timer, &to, lldp_port_timeout, lp, true); + if (err != EDPVS_OK) { + lldp_port_unhash(lp); + rte_free(lp); + return err; + } + + return EDPVS_OK; +} + +static struct lldp_entry *lldp_entry_get(const struct lldp_port *port, lldp_type_t type) +{ + struct lldp_entry *e; + + if (unlikely(NULL == port)) + return NULL; + + list_for_each_entry(e, &port->head, node) { + if (lldp_type_equal(e->type, type)) + return e; + } + return NULL; +} + +static void lldp_entry_hash(struct lldp_entry *e, struct lldp_port *port) +{ + struct lldp_entry *entry, *next = NULL; + + /* put LLDP_TYPE_END node at tail */ + if (unlikely(!e->type.type)) { + list_add_tail(&e->node, &port->head); + ++port->entries; + return; + } + + list_for_each_entry(entry, &port->head, node) { + if (!entry->type.type || lldp_type_cmp(&entry->type, &e->type) >= 0) { + next = entry; + break; + } + } + + if (NULL != next) + list_add_tail(&e->node, &next->node); + else + list_add_tail(&e->node, &port->head); + ++port->entries; +} + +static inline void lldp_entry_unhash(struct lldp_entry *e) +{ + list_del_init(&e->node); + --e->port->entries; +} + +static int lldp_entry_del(struct lldp_entry *entry) +{ + lldp_entry_unhash(entry); + rte_free(entry); + return EDPVS_OK; +} + +static int lldp_entry_add(struct lldp_port *port, char *lldpdu) +{ + int err; + lldp_type_t type; + uint16_t len; + struct lldp_entry *entry; + struct lldp_type_ops *ops; + + type.type = DPVS_LLDP_TL_TYPE((uint16_t)(*lldpdu)); + ops = lldp_type_get(type); + if (!ops) + return EDPVS_NOTSUPP; + err = ops->parse_type(lldpdu, &type, &len); + if (EDPVS_OK != err) + return err; + assert(len <= DPVS_LLDP_PDU_MAX); + + entry = lldp_entry_get(port, type); + if (entry) { + /* do update */ + if (entry->len >= len) { + entry->len = len; + entry->stale = 0; + rte_memcpy(&entry->typelen, lldpdu, len + 2); + if (ops->on_change) + return ops->on_change(entry); + return EDPVS_OK; + } + lldp_entry_del(entry); + } + + entry = rte_zmalloc("lldp_entry", sizeof(struct lldp_entry) + len + 2, RTE_CACHE_LINE_SIZE); + if (unlikely(!entry)) + return EDPVS_NOMEM; + entry->type = type; + entry->len = len; + entry->port = port; + rte_memcpy(&entry->typelen, lldpdu, len + 2); + + lldp_entry_hash(entry, port); + + if (ops->on_change) + return ops->on_change(entry); + return EDPVS_OK; +} + +static int lldp_dump_pdu(const struct lldp_port *port, char *buf, size_t buflen) +{ + int rc; + size_t room; + char *ptr; + struct lldp_entry *e; + struct lldp_type_ops *ops; + + ptr = buf; + room = buflen; + list_for_each_entry(e, &port->head, node) { + if (room <= 0) + return EDPVS_NOROOM; + ops = lldp_type_get(e->type); + if (unlikely(!ops)) + return EDPVS_NOTSUPP; + if (ops->dump) { + rc = ops->dump(e, ptr, room); + if (unlikely(rc < 0)) + return rc; + if (unlikely(rc > room)) + return EDPVS_NOROOM; + ptr += rc; + room -= rc; + } + } + + return EDPVS_OK; +} + +static int lldp_pdu_local_update(struct netif_port *dev, bool in_timer) +{ + int i, rc; + struct lldp_port *port; + struct lldp_type_ops *ops; + char buf[DPVS_LLDP_PDU_MAX]; + + static lldp_type_t local_lldp_types[] = { + { LLDP_TYPE_CHASSIS_ID, LLDP_CHASSIS_ID_MAC_ADDRESS }, + { LLDP_TYPE_PORT_ID, LLDP_PORT_ID_INTERFACE_NAME }, + { LLDP_TYPE_TTL, 0 }, + { LLDP_TYPE_PORT_DESC, 0 }, + { LLDP_TYPE_SYS_NAME, 0 }, + { LLDP_TYPE_SYS_DESC, 0 }, + { LLDP_TYPE_SYS_CAP, 0 }, + { LLDP_TYPE_MNG_ADDR, 1 }, /* ipv4 */ + { LLDP_TYPE_MNG_ADDR, 2 }, /* ipv6 */ + { LLDP_TYPE_END, 0 }, + }; + + port = lldp_port_get(dev->id, DPVS_LLDP_NODE_LOCAL); + if (!port) { + /* timeout of 3*DPVS_LLDP_UPDATE_INTERVA ensures local lldp caches persist */ + rc = lldp_port_add(dev, DPVS_LLDP_NODE_LOCAL, 3 * DPVS_LLDP_UPDATE_INTERVAL, in_timer); + if (unlikely(EDPVS_OK != rc)) + return rc; + port = lldp_port_get(dev->id, DPVS_LLDP_NODE_LOCAL); + assert(port != NULL); + } + + for (i = 0; i < NELEMS(local_lldp_types); i++) { + ops = lldp_type_get(local_lldp_types[i]); + if (!ops || !ops->local_lldp) + continue; + rc = ops->local_lldp(dev, local_lldp_types[i].subtype, buf, sizeof(buf)); + if (unlikely(rc < 0)) { + RTE_LOG(INFO, LLDP, "%s: fail to generate local lldp pdu, type %d.%d," + " err %s\n", __func__, local_lldp_types[i].type, + local_lldp_types[i].subtype, dpvs_strerror(rc)); + continue; + } + if (unlikely(rc > sizeof(buf))) + return EDPVS_NOROOM; + rc = lldp_entry_add(port, buf); + if (EDPVS_OK != rc) + return rc; + } + + if (in_timer) + dpvs_timer_reset_nolock(&port->timer, true); + else + dpvs_timer_reset(&port->timer, true); + + return EDPVS_OK; +} + +static int lldp_pdu_neigh_update(struct netif_port *dev, const struct rte_mbuf *mbuf, bool in_timer) +{ + int err; + char *ptr; + size_t totlen; + uint16_t typelen; + uint16_t len; + uint8_t type; + bool check_stale = false; + struct lldp_port *port; + struct lldp_entry *entry, *next; + struct timeval timeout; + + port = lldp_port_get(dev->id, DPVS_LLDP_NODE_NEIGH); + if (!port) { + err = lldp_port_add(dev, DPVS_LLDP_NODE_NEIGH, DPVS_LLDP_TTL_DEFAULT, in_timer); + if (unlikely(EDPVS_OK != err)) + return err; + port = lldp_port_get(dev->id, DPVS_LLDP_NODE_NEIGH); + assert(port != NULL); + } else { + check_stale = true; + list_for_each_entry(entry, &port->head, node) + entry->stale = 1; + } + + totlen = mbuf->data_len; + ptr = rte_pktmbuf_mtod(mbuf, char *); + while (totlen > 0) { + typelen = *((uint16_t*)ptr); + type = DPVS_LLDP_TL_TYPE(typelen); + len = DPVS_LLDP_TL_LEN(typelen) + 2; + err = lldp_entry_add(port, ptr); + if (unlikely(EDPVS_OK != err && EDPVS_NOTSUPP != err)) + return err; + totlen -= len; + ptr += len; + if (LLDP_TYPE_END == type) + break; + } + + if (check_stale) { + list_for_each_entry_safe(entry, next, &port->head, node) { + if (entry->stale) + lldp_entry_del(entry); + } + } + + timeout.tv_sec = port->timeout; + dpvs_time_rand_delay(&timeout, 1000000); + if (in_timer) + err = dpvs_timer_update_nolock(&port->timer, &timeout, true); + else + err = dpvs_timer_update(&port->timer, &timeout, true); + return err; +} + +static int lldp_local_update_all(void *arg) +{ + int err; + portid_t i, start, end; + struct netif_port *dev; + + RTE_LOG(DEBUG, LLDP, "%s: updating local lldp cache\n", __func__); + + netif_physical_port_range(&start, &end); + for (i = start; i < end; i++) { + dev = netif_port_get(i); + assert(dev != NULL); + if (!(dev->flag & NETIF_PORT_FLAG_LLDP)) + continue; + err = lldp_pdu_local_update(dev, true); + if (EDPVS_OK != err) + RTE_LOG(WARNING, LLDP, "%s: fail to update local lldp cache on port %s: %s\n", + __func__, dev->name, dpvs_strerror(err)); + } + + return DTIMER_OK; +} + +static int lldp_xmit(struct netif_port *dev, bool in_timer) +{ + int err; + char *ptr; + struct rte_mbuf *mbuf; + struct lldp_port *port; + struct lldp_entry *entry; + struct rte_ether_hdr *ehdr; + + port = lldp_port_get(dev->id, DPVS_LLDP_NODE_LOCAL); + if (!port || port->entries <= 0) { + err = lldp_pdu_local_update(dev, in_timer); // FIXME: update lldp cache asynchronously + if (EDPVS_OK != err) { + RTE_LOG(ERR, LLDP, "%s: lldp_pdu_local_update failed: %s\n", + __func__, dpvs_strerror(err)); + return err; + } + port = lldp_port_get(dev->id, DPVS_LLDP_NODE_LOCAL); + if (unlikely(!port)) + return EDPVS_NOTEXIST; + if (port->entries <= 0) + return EDPVS_OK; + } + + mbuf = rte_pktmbuf_alloc(dev->mbuf_pool); + if (unlikely(!mbuf)) + return EDPVS_NOMEM; + mbuf_userdata_reset(mbuf); + + list_for_each_entry(entry, &port->head, node) { + ptr = rte_pktmbuf_append(mbuf, entry->len + 2); + if (unlikely(!ptr)) + return EDPVS_NOROOM; + rte_memcpy(ptr, &entry->typelen, entry->len + 2); + } + + ehdr = (struct rte_ether_hdr *)rte_pktmbuf_prepend(mbuf, sizeof(*ehdr)); + if (unlikely(!ptr)) + return EDPVS_NOROOM; + rte_memcpy(&ehdr->d_addr, &LLDP_ETHER_ADDR_DST, sizeof(ehdr->d_addr)); + rte_memcpy(&ehdr->s_addr, &dev->addr, sizeof(ehdr->s_addr)); + ehdr->ether_type = rte_cpu_to_be_16(RTE_ETHER_TYPE_LLDP); + + if (dev->type == PORT_TYPE_BOND_SLAVE) { + // FIXME: + // How to send LLDP packet on a specified slave port? I found no solutions to it via + // DPDK API. Maybe changes should be made to bond PMD driver to solve the problem. + // So I save the slave port id in mbuf, and hope bond PMD driver may consider it when + // distributing mbufs to slave ports. + // + // Store the slave port id into mbuf->port? + // No! mbuf->port is reset to the bond master's port id in the forthcoming transmit process. + // Use mbuf->hash.txadapter.reserved2 instead. Hope no conflictions. Remember to reset it to + // RTE_MBUF_PORT_INVALID in rte_pktmbuf_alloc. + // + mbuf->hash.txadapter.reserved2 = dev->id; + //MBUF_USERDATA(mbuf, portid_t, MBUF_FIELD_ORIGIN_PORT) = port->id; + dev = dev->bond->slave.master; + } + + return netif_xmit(mbuf, dev); + +} + +static int lldp_xmit_all(void *arg) +{ + int err; + portid_t i, start, end; + struct netif_port *dev; + + netif_physical_port_range(&start, &end); + for (i = start; i < end; i++) { + dev = netif_port_get(i); + assert(dev != NULL); + if (!(dev->flag & NETIF_PORT_FLAG_LLDP)) + continue; + err = lldp_xmit(dev, true); + if (EDPVS_OK != err) + RTE_LOG(WARNING, LLDP, "%s: fail to xmit lldp frame on port %s: %s\n", + __func__, dev->name, dpvs_strerror(err)); + } + + return DTIMER_OK; +} + +static int lldp_ether_addr_filter(bool add) +{ + int err; + portid_t i, start, end; + struct netif_port *dev; + + netif_physical_port_range(&start, &end); + for (i = start; i < end; i++) { + dev = netif_port_get(i); + assert(dev != NULL); + if (add) + err = netif_mc_add(dev, &LLDP_ETHER_ADDR_DST); + else + err = netif_mc_del(dev, &LLDP_ETHER_ADDR_DST); + if (err != EDPVS_OK) + return err; + } + + return EDPVS_OK; +} + +static int lldp_xmit_start(void) +{ + int err; + struct timeval timeout1 = { .tv_sec = DPVS_LLDP_TX_INTERVAL }; + struct timeval timeout2 = { .tv_sec = DPVS_LLDP_UPDATE_INTERVAL }; + + assert(rte_lcore_id() == rte_get_main_lcore()); + + err = lldp_ether_addr_filter(true); + if (EDPVS_OK != err && EDPVS_EXIST != err) { + RTE_LOG(WARNING, LLDP, "%s: failed to add lldp multicast ether address -- %s\n", + __func__, dpvs_strerror(err)); + return err; + } + + dpvs_time_rand_delay(&timeout1, 1000000); + err = dpvs_timer_sched_period(&lldp_xmit_timer, &timeout1, lldp_xmit_all, NULL, true); + if (EDPVS_OK != err) { + RTE_LOG(WARNING, LLDP, "%s: failed to schedule lldp_xmit_timer -- %s\n", + __func__, dpvs_strerror(err)); + lldp_ether_addr_filter(false); + return err; + } + + dpvs_time_rand_delay(&timeout2, 1000000); + err = dpvs_timer_sched_period(&lldp_update_timer, &timeout2, lldp_local_update_all, NULL, true); + if (EDPVS_OK != err) { + RTE_LOG(WARNING, LLDP, "%s: failed to schedule lldp_update_timer -- %s\n", + __func__, dpvs_strerror(err)); + dpvs_timer_cancel(&lldp_xmit_timer, true); + lldp_ether_addr_filter(false); + return err; + } + + return EDPVS_OK; +} + +static int lldp_xmit_stop(void) +{ + int err; + + assert(rte_lcore_id() == rte_get_main_lcore()); + + err = lldp_ether_addr_filter(false); + if (EDPVS_OK != err && EDPVS_NOTEXIST != err) { + RTE_LOG(WARNING, LLDP, "%s: failed to del lldp multicast ether address -- %s\n", + __func__, dpvs_strerror(err)); + return err; + } + + err = dpvs_timer_cancel(&lldp_xmit_timer, true); + if (EDPVS_OK != err) { + RTE_LOG(ERR, LLDP, "%s: failed to cancel lldp_xmit_timer -- %s\n", + __func__, dpvs_strerror(err)); + return err; + } + + err = dpvs_timer_cancel(&lldp_update_timer, true); + if (EDPVS_OK != err) { + RTE_LOG(ERR, LLDP, "%s: failed to cancel lldp_update_timer -- %s\n", + __func__, dpvs_strerror(err)); + return err; + } + + return EDPVS_OK; +} + +static int lldp_rcv(struct rte_mbuf *mbuf, struct netif_port *dev) +{ + int err; + portid_t pid; + static uint32_t seq = 0; + struct dpvs_msg *msg; + + if (!lldp_enable) + return EDPVS_KNICONTINUE; + + if (is_bond_port(dev->id)) { + pid = MBUF_USERDATA(mbuf, portid_t, MBUF_FIELD_ORIGIN_PORT); + dev = netif_port_get(pid); + if (unlikely(NULL == dev)) { + RTE_LOG(WARNING, LLDP, "%s: fail to find lldp physical device of port id %d\n", + __func__, pid); + rte_pktmbuf_free(mbuf); + return EDPVS_RESOURCE; + } + } + if (!(dev->flag & NETIF_PORT_FLAG_LLDP)) + return EDPVS_KNICONTINUE; + + /* redirect lldp mbuf to master lcore */ + msg = msg_make(MSG_TYPE_LLDP_RECV, seq++, DPVS_MSG_UNICAST, + rte_lcore_id(), sizeof(void *), &mbuf); + if (unlikely(NULL == msg)) { + rte_pktmbuf_free(mbuf); + return EDPVS_NOMEM; + } + + err = msg_send(msg, rte_get_main_lcore(), DPVS_MSG_F_ASYNC, NULL); + if (unlikely(EDPVS_OK != err)) { + RTE_LOG(WARNING, LLDP, "%s: fail to send mbuf to master lcore!\n", __func__); + rte_pktmbuf_free(mbuf); + } + msg_destroy(&msg); + return err; +} + +static int lldp_rcv_msg_cb(struct dpvs_msg *msg) +{ + int err; + portid_t pid, start, end; + struct netif_port *dev; + struct rte_mbuf *mbuf; + + mbuf = *(struct rte_mbuf **)(msg->data); + + pid = mbuf->port; + netif_bond_port_range(&start, &end); + if (pid < end && pid >= start) + pid = MBUF_USERDATA(mbuf, portid_t, MBUF_FIELD_ORIGIN_PORT); + + dev = netif_port_get(pid); + if (unlikely(NULL == dev)) { + RTE_LOG(WARNING, LLDP, "%s: fail to find lldp physical device of port id %d\n", + __func__, pid); + rte_pktmbuf_free(mbuf); + return EDPVS_RESOURCE; + } + + err = lldp_pdu_neigh_update(dev, mbuf, false); + rte_pktmbuf_free(mbuf); /* always consume the mbuf */ + return err; +} + +static int lldp_rcv_msg_register(void) +{ + lcoreid_t master_cid = rte_get_main_lcore(); + struct dpvs_msg_type mt = { + .type = MSG_TYPE_LLDP_RECV, + .mode = DPVS_MSG_UNICAST, + .prio = MSG_PRIO_LOW, + .cid = master_cid, + .unicast_msg_cb = lldp_rcv_msg_cb, + }; + + return msg_type_register(&mt); +} + +static int lldp_rcv_msg_unregister(void) +{ + lcoreid_t master_cid = rte_get_main_lcore(); + struct dpvs_msg_type mt = { + .type = MSG_TYPE_LLDP_RECV, + .mode = DPVS_MSG_UNICAST, + .prio = MSG_PRIO_LOW, + .cid = master_cid, + .unicast_msg_cb = lldp_rcv_msg_cb, + }; + + return msg_type_unregister(&mt); +} + +static int lldp_sockopt_set(sockoptid_t opt, const void *conf, size_t size) +{ + // TODO + return EDPVS_NOTSUPP; +} + +static int lldp_sockopt_get(sockoptid_t opt, const void *conf, size_t size, + void **out, size_t *outsize) +{ + const struct lldp_param *param = conf; + struct lldp_message *message; + struct netif_port *dev; + struct lldp_port *port; + int err; + + *outsize = 0; + *out = NULL; + + if (!conf || size < sizeof(*param) || !out || !outsize) + return EDPVS_INVAL; + + if (opt != SOCKOPT_GET_LLDP_SHOW) + return EDPVS_NOTSUPP; + + dev = netif_port_get_by_name(param->ifname); + if (!dev) { + RTE_LOG(WARNING, LLDP, "%s: no such device\n", __func__); + return EDPVS_NODEV; + } + + if (param->node >= DPVS_LLDP_NODE_MAX) { + RTE_LOG(WARNING, LLDP, "%s: invalid node type %d, only supports type " + "local(%d) and neigh(%d)\n", __func__, param->node, + DPVS_LLDP_NODE_LOCAL, DPVS_LLDP_NODE_NEIGH); + return EDPVS_INVAL; + } + + port = lldp_port_get(dev->id, param->node); + if (!port) { + RTE_LOG(INFO, LLDP, "%s: %s lldp port on %s not found!\n", __func__, + param->node == DPVS_LLDP_NODE_NEIGH ? "neighbor" : "local", dev->name); + return EDPVS_NOTEXIST; + } + + message = rte_calloc(NULL, 1, sizeof(*message), 0); + if (!message) + return EDPVS_NOMEM; + rte_memcpy(&message->param, param, sizeof(*param)); + err = lldp_dump_pdu(port, message->message, sizeof(message->message)); + if (EDPVS_OK != err) { + RTE_LOG(WARNING, LLDP, "%s: lldp_dump_pdu failed -- %s\n", + __func__, dpvs_strerror(err)); + rte_free(message); + return err; + } + + *out = message; + *outsize = sizeof(*message); + return EDPVS_OK; +} + +static struct dpvs_sockopts lldp_sockopts = { + .version = SOCKOPT_VERSION, + .set_opt_min = SOCKOPT_SET_LLDP_TODO, + .set_opt_max = SOCKOPT_SET_LLDP_TODO, + .set = lldp_sockopt_set, + .get_opt_min = SOCKOPT_GET_LLDP_SHOW, + .get_opt_max = SOCKOPT_GET_LLDP_SHOW, + .get = lldp_sockopt_get, +}; + +static struct lldp_type_ops lldp_ops[] = { + { + .type = LLDP_TYPE_END, + .parse_type = lldp_parse_type_default, + .local_lldp = lldp_local_pdu_end, + .dump = lldp_dump_end, + }, + { + .type = LLDP_TYPE_CHASSIS_ID, + .parse_type = lldp_parse_type_chassis_id, + .local_lldp = lldp_local_pdu_chassis_id, + .dump = lldp_dump_chassis_id, + }, + { + .type = LLDP_TYPE_PORT_ID, + .parse_type = lldp_parse_type_port_id, + .local_lldp = lldp_local_pdu_port_id, + .dump = lldp_dump_port_id, + }, + { + .type = LLDP_TYPE_TTL, + .parse_type = lldp_parse_type_default, + .local_lldp = lldp_local_pdu_ttl, + .dump = lldp_dump_ttl, + .on_change = lldp_on_change_ttl, + }, + { + .type = LLDP_TYPE_PORT_DESC, + .parse_type = lldp_parse_type_default, + .local_lldp = lldp_local_pdu_port_desc, + .dump = lldp_dump_port_desc, + }, + { + .type = LLDP_TYPE_SYS_NAME, + .parse_type = lldp_parse_type_default, + .local_lldp = lldp_local_pdu_sys_name, + .dump = lldp_dump_sys_name, + }, + { + .type = LLDP_TYPE_SYS_DESC, + .parse_type = lldp_parse_type_default, + .local_lldp = lldp_local_pdu_sys_desc, + .dump = lldp_dump_sys_desc, + }, + { + .type = LLDP_TYPE_SYS_CAP, + .parse_type = lldp_parse_type_default, + .local_lldp = lldp_local_pdu_sys_cap, + .dump = lldp_dump_sys_cap, + }, + { + .type = LLDP_TYPE_MNG_ADDR, + .parse_type = lldp_parse_type_mng_addr, + .local_lldp = lldp_local_pdu_mng_addr, + .dump = lldp_dump_mng_addr, + }, + { + .type = LLDP_TYPE_ORG, + .parse_type = lldp_parse_type_org, + .local_lldp = NULL, + .dump = lldp_dump_org_specific, + } +}; + +static struct pkt_type dpvs_lldp_pkt_type = { + //.type = rte_cpu_to_be_16(RTE_ETHER_TYPE_LLDP), + .func = lldp_rcv, + .port = NULL, +}; + +int dpvs_lldp_init(void) +{ + int i, err; + + lldp_serail_number_init(); + + if (unlikely(uname(&lldp_uname) < 0)) + return EDPVS_SYSCALL; + + for (i = 0; i < DPVS_LLDP_NODE_MAX; i++) + INIT_LIST_HEAD(&lldp_ports[i]); + + for (i = 0; i < NELEMS(lldp_ops); i++) { + err = lldp_type_register(&lldp_ops[i]); + assert(EDPVS_OK == err); + } + + err = lldp_rcv_msg_register(); + if (EDPVS_OK != err) + goto unreg_lldp_ops; + + err = sockopt_register(&lldp_sockopts); + if (EDPVS_OK != err) + goto unreg_msg; + + dpvs_lldp_pkt_type.type = rte_cpu_to_be_16(RTE_ETHER_TYPE_LLDP); + err = netif_register_pkt(&dpvs_lldp_pkt_type); + if (EDPVS_OK != err) + goto unreg_sockopt; + + if (lldp_enable) { + err = lldp_xmit_start(); + if (EDPVS_OK != err) + goto unreg_pkttype; + } + + return EDPVS_OK; + +unreg_pkttype: + netif_unregister_pkt(&dpvs_lldp_pkt_type); +unreg_sockopt: + sockopt_unregister(&lldp_sockopts); +unreg_msg: + lldp_rcv_msg_unregister(); +unreg_lldp_ops: + for (i = 0; i < NELEMS(lldp_ops); i++) + lldp_type_unregister(&lldp_ops[i]); + return err; +} + +int dpvs_lldp_term(void) +{ + int i, err; + + if (lldp_enable) + lldp_xmit_stop(); + + dpvs_lldp_pkt_type.type = rte_cpu_to_be_16(RTE_ETHER_TYPE_LLDP); + err = netif_unregister_pkt(&dpvs_lldp_pkt_type); + if (EDPVS_OK != err) + RTE_LOG(WARNING, LLDP, "%s: fail to unregister lldp packet type\n", __func__); + + err = sockopt_unregister(&lldp_sockopts); + if (EDPVS_OK != err) + RTE_LOG(WARNING, LLDP, "%s: fail to unregister lldp msg\n", __func__); + err = lldp_rcv_msg_unregister(); + if (EDPVS_OK != err) + RTE_LOG(WARNING, LLDP, "%s: fail to unregister lldp msg\n", __func__); + + for (i = 0; i < NELEMS(lldp_ops); i++) { + err = lldp_type_unregister(&lldp_ops[i]); + if (EDPVS_OK != err) + RTE_LOG(WARNING, LLDP, "%s: lldp_type_unregister(%d) failed\n", __func__, i); + } + + return EDPVS_OK; +} diff --git a/src/main.c b/src/main.c index 8f8067c0f..5cf291a24 100644 --- a/src/main.c +++ b/src/main.c @@ -311,6 +311,7 @@ int main(int argc, char *argv[]) gettimeofday(&tv, NULL); srandom(tv.tv_sec ^ tv.tv_usec ^ getpid()); + srand48(tv.tv_sec ^ tv.tv_usec ^ getpid()); rte_srand((uint64_t)(tv.tv_sec ^ tv.tv_usec ^ getpid())); sys_start_time(); diff --git a/src/mbuf.c b/src/mbuf.c index 7d10d54d3..8c913eb36 100644 --- a/src/mbuf.c +++ b/src/mbuf.c @@ -209,6 +209,11 @@ int mbuf_init(void) .size = sizeof(mbuf_userdata_field_route_t), .align = 8, }, + [ MBUF_FIELD_ORIGIN_PORT ] = { + .name = "origin_port", + .size = sizeof(portid_t), + .align = 2, + }, }; for (i = 0; i < NELEMS(rte_mbuf_userdata_fields); i++) { diff --git a/src/netif.c b/src/netif.c index a40252f21..3fd24aa0b 100644 --- a/src/netif.c +++ b/src/netif.c @@ -161,16 +161,32 @@ static struct list_head port_ntab[NETIF_PORT_TABLE_BUCKETS]; /* hashed by name * /* function declarations */ static void kni_lcore_loop(void *dummy); -static inline bool is_physical_port(portid_t pid) +bool is_physical_port(portid_t pid) { return pid >= phy_pid_base && pid < phy_pid_end; } -static inline bool is_bond_port(portid_t pid) +bool is_bond_port(portid_t pid) { return pid >= bond_pid_base && pid < bond_pid_end; } +void netif_physical_port_range(portid_t *start, portid_t *end) +{ + if (start) + *start = phy_pid_base; + if (end) + *end = phy_pid_end; +} + +void netif_bond_port_range(portid_t *start, portid_t *end) +{ + if (start) + *start = bond_pid_base; + if (end) + *end = bond_pid_end; +} + bool is_lcore_id_valid(lcoreid_t cid) { if (unlikely(cid >= DPVS_MAX_LCORE)) @@ -2536,6 +2552,10 @@ void lcore_process_packets(struct rte_mbuf **mbufs, lcoreid_t cid, uint16_t coun lcore_stats[cid].dropped++; continue; } + + /* some protocols like LLDP may still like the originated port */ + MBUF_USERDATA(mbuf, portid_t, MBUF_FIELD_ORIGIN_PORT) = mbuf->port; + if (dev->type == PORT_TYPE_BOND_SLAVE) { dev = dev->bond->slave.master; mbuf->port = dev->id; @@ -3448,6 +3468,10 @@ static inline void setup_dev_of_flags(struct netif_port *port) } if (port->dev_info.rx_offload_capa & DEV_RX_OFFLOAD_IPV4_CKSUM) port->flag |= NETIF_PORT_FLAG_RX_IP_CSUM_OFFLOAD; + + /* enable lldp on physical port */ + if (is_physical_port(port->id)) + port->flag |= NETIF_PORT_FLAG_LLDP; } /* TODO: refactor it with netif_alloc */ @@ -4832,6 +4856,8 @@ static int get_port_basic(struct netif_port *port, void **out, size_t *out_len) get->ol_tx_tcp_csum = 1; if (port->flag & NETIF_PORT_FLAG_TX_UDP_CSUM_OFFLOAD) get->ol_tx_udp_csum = 1; + if (port->flag & NETIF_PORT_FLAG_LLDP) + get->lldp = 1; *out = get; *out_len = sizeof(netif_nic_basic_get_t); @@ -5317,6 +5343,11 @@ static int set_port(struct netif_port *port, const netif_nic_set_t *port_cfg) else if (port_cfg->tc_ingress_off) port->flag &= (~NETIF_PORT_FLAG_TC_INGRESS); + if (port_cfg->lldp_on) + port->flag |= NETIF_PORT_FLAG_LLDP; + else if (port_cfg->lldp_off) + port->flag &= (~NETIF_PORT_FLAG_LLDP); + return EDPVS_OK; } diff --git a/src/vlan.c b/src/vlan.c index 1b06aea3b..6d657e449 100644 --- a/src/vlan.c +++ b/src/vlan.c @@ -241,6 +241,7 @@ int vlan_add_dev(struct netif_port *real_dev, const char *ifname, dev->flag &= ~NETIF_PORT_FLAG_TX_IP_CSUM_OFFLOAD; dev->flag &= ~NETIF_PORT_FLAG_TX_TCP_CSUM_OFFLOAD; dev->flag &= ~NETIF_PORT_FLAG_TX_UDP_CSUM_OFFLOAD; + dev->flag &= ~NETIF_PORT_FLAG_LLDP; dev->type = PORT_TYPE_VLAN; rte_ether_addr_copy(&real_dev->addr, &dev->addr); diff --git a/tools/dpip/Makefile b/tools/dpip/Makefile index 4dc648e38..3ed2aaf15 100644 --- a/tools/dpip/Makefile +++ b/tools/dpip/Makefile @@ -40,7 +40,7 @@ DEFS = -D DPVS_MAX_LCORE=64 -D DPIP_VERSION=\"$(VERSION_STRING)\" CFLAGS += $(DEFS) OBJS = ipset.o dpip.o utils.o route.o addr.o neigh.o link.o vlan.o maddr.o \ - qsch.o cls.o tunnel.o ipset.o ipv6.o iftraf.o eal_mem.o flow.o \ + qsch.o cls.o tunnel.o ipset.o ipv6.o iftraf.o eal_mem.o flow.o lldp.o \ ../../src/common.o ../keepalived/keepalived/check/sockopt.o all: $(TARGET) diff --git a/tools/dpip/dpip.c b/tools/dpip/dpip.c index a3b31f1c6..1249e0b3b 100644 --- a/tools/dpip/dpip.c +++ b/tools/dpip/dpip.c @@ -35,7 +35,7 @@ static void usage(void) " "DPIP_NAME" [OPTIONS] OBJECT { COMMAND | help }\n" "Parameters:\n" " OBJECT := { link | addr | route | neigh | vlan | tunnel | qsch | cls |\n" - " ipv6 | iftraf | eal-mem | ipset | flow | maddr }\n" + " ipv6 | iftraf | eal-mem | ipset | flow | maddr | lldp }\n" " COMMAND := { create | destroy | add | del | show (list) | set (change) |\n" " replace | flush | test | enable | disable }\n" "Options:\n" diff --git a/tools/dpip/link.c b/tools/dpip/link.c index fd3d5bd84..acaab26b5 100644 --- a/tools/dpip/link.c +++ b/tools/dpip/link.c @@ -127,7 +127,7 @@ static void link_help(void) " dpip link set DEV-NAME ITEM VALUE\n" " ---supported items---\n" - " promisc [on|off], forward2kni [on|off], link [up|down],\n" + " promisc [on|off], forward2kni [on|off], link [up|down], lldp [up|down]\n" " allmulticast [on|off], tc-egress [on|off], tc-ingress [on|off], addr, \n" " bond-[mode|slave|primary|xmit-policy|monitor-interval|link-up-prop|" "link-down-prop]\n" @@ -260,6 +260,9 @@ static int dump_nic_basic(char *name, int namelen) if (get.tc_ingress) printf("tc-ingress "); + if (get.lldp) + printf("lldp "); + printf("\n"); printf(" addr %s ", get.addr); @@ -958,6 +961,25 @@ static int link_nic_set_tc_ingress(const char *name, const char *value) return dpvs_setsockopt(SOCKOPT_NETIF_SET_PORT, &cfg, sizeof(netif_nic_set_t)); } +static int link_nic_set_lldp(const char *name, const char *value) +{ + netif_nic_set_t cfg = {}; + assert(value); + + snprintf(cfg.pname, sizeof(cfg.pname), "%s", name); + + if (strcmp(value, "on") == 0) + cfg.lldp_on = 1; + else if(strcmp(value, "off") == 0) + cfg.lldp_off = 1; + else { + fprintf(stderr, "invalid arguement value for 'lldp'\n"); + return EDPVS_INVAL; + } + + return dpvs_setsockopt(SOCKOPT_NETIF_SET_PORT, &cfg, sizeof(netif_nic_set_t)); +} + static int link_bond_add_bond_slave(const char *name, const char *value) { netif_bond_set_t cfg; @@ -1193,6 +1215,8 @@ static int link_set(struct link_param *param) link_nic_set_tc_egress(param->dev_name, param->value); else if (strcmp(param->item, "tc-ingress") == 0) link_nic_set_tc_ingress(param->dev_name, param->value); + else if (strcmp(param->item, "lldp") == 0) + link_nic_set_lldp(param->dev_name, param->value); else { fprintf(stderr, "invalid parameter name '%s'\n", param->item); return EDPVS_INVAL; diff --git a/tools/dpip/lldp.c b/tools/dpip/lldp.c new file mode 100644 index 000000000..aed52c91c --- /dev/null +++ b/tools/dpip/lldp.c @@ -0,0 +1,128 @@ +/* + * DPVS is a software load balancer (Virtual Server) based on DPDK. + * + * Copyright (C) 2021 iQIYI (www.iqiyi.com). + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +#include +#include "dpip.h" +#include "sockopt.h" +#include "conf/lldp.h" + +static void lldp_help(void) +{ + fprintf(stderr, + "Usage:\n" + " dpip lldp show TYPE dev NAME\n" + " TYPE := [ local | neigh ]\n" + " NAME := interface name\n" + "Examples:\n" + " dpip lldp show local dev dpdk0\n" + " dpip lldp show dev dpdk1 neigh\n"); +} + +static int lldp_parse(struct dpip_obj *obj, struct dpip_conf *conf) +{ + struct lldp_param *param = obj->param; + + memset(param, 0, sizeof(*param)); + + while (conf->argc > 0) { + if (strcmp(conf->argv[0], "dev") == 0) { + NEXTARG_CHECK(conf, conf->argv[0]); + snprintf(param->ifname, sizeof(param->ifname), "%s", conf->argv[0]); + } else { + if (strcmp(conf->argv[0], "local") == 0) { + param->node = DPVS_LLDP_NODE_LOCAL; + } else if (strcmp(conf->argv[0], "neigh") == 0) { + param->node = DPVS_LLDP_NODE_NEIGH; + } else { + fprintf(stderr, "too many arguments\n"); + return EDPVS_INVAL; + } + } + NEXTARG(conf); + } + + return EDPVS_OK; +} + +static int lldp_check(const struct dpip_obj *obj, dpip_cmd_t cmd) +{ + const struct lldp_param *param = obj->param; + + /* sanity check */ + switch (cmd) { + case DPIP_CMD_SHOW: + if (strlen(param->ifname) == 0) { + fprintf(stderr, "missing device name\n"); + return EDPVS_INVAL; + } + return EDPVS_OK; + default: + return EDPVS_NOTSUPP; + } + return EDPVS_OK; +} + +static int lldp_do_cmd(struct dpip_obj *obj, dpip_cmd_t cmd, struct dpip_conf *conf) +{ + const struct lldp_param *param = obj->param; + struct lldp_message *message; + size_t size; + int err; + + switch (cmd) { + case DPIP_CMD_SHOW: + err = dpvs_getsockopt(SOCKOPT_GET_LLDP_SHOW, param, sizeof(*param), + (void **)&message, &size); + if (err != EDPVS_OK) + return err; + + if (size < sizeof(*message)) { + fprintf(stderr, "corrupted response\n"); + dpvs_sockopt_msg_free(message); + return EDPVS_INVAL; + } + printf("-*-*-*- %s LLDP Message on Port %s -*-*-*-\n", + message->param.node == DPVS_LLDP_NODE_NEIGH ? "Neighbour" : "Local", + message->param.ifname); + printf(message->message); + dpvs_sockopt_msg_free(message); + return EDPVS_OK; + default: + return EDPVS_NOTSUPP; + } +} + +static struct lldp_param lldp_param; + +static struct dpip_obj dpip_lldp = { + .name = "lldp", + .param = &lldp_param, + .help = lldp_help, + .parse = lldp_parse, + .check = lldp_check, + .do_cmd = lldp_do_cmd, +}; + +static void __init lldp_init(void) +{ + dpip_register_obj(&dpip_lldp); +} + +static void __exit lldp_exit(void) +{ + dpip_unregister_obj(&dpip_lldp); +} From 9baec6d6c91e073b4279c2c90945785c3eaa173f Mon Sep 17 00:00:00 2001 From: ywc689 Date: Tue, 30 Jul 2024 15:58:09 +0800 Subject: [PATCH 41/63] patch: lldp bonding xmit patch for dpdk Signed-off-by: ywc689 --- ...ends-packets-with-user-specified-sal.patch | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 patch/dpdk-stable-20.11.1/0007-bonding-device-sends-packets-with-user-specified-sal.patch diff --git a/patch/dpdk-stable-20.11.1/0007-bonding-device-sends-packets-with-user-specified-sal.patch b/patch/dpdk-stable-20.11.1/0007-bonding-device-sends-packets-with-user-specified-sal.patch new file mode 100644 index 000000000..d7e4e0c6d --- /dev/null +++ b/patch/dpdk-stable-20.11.1/0007-bonding-device-sends-packets-with-user-specified-sal.patch @@ -0,0 +1,91 @@ +From 7024d80414e914a54c301dbcc9bb4cf6fb5f927b Mon Sep 17 00:00:00 2001 +From: yuwenchao +Date: Tue, 30 Jul 2024 15:39:28 +0800 +Subject: [PATCH] bonding device sends packets with user specified salve port + +The outgoing slave port is specified in mbuf field "hash.txadapter.reserved2". +Support the following 3 bonding mode: +- mode 0: round robin +- mode 2: balance +- mode 4: 8023ad + +Signed-off-by: yuwenchao +--- + drivers/net/bonding/rte_eth_bond_pmd.c | 26 ++++++++++++++++++++++++-- + lib/librte_mbuf/rte_mbuf.h | 2 ++ + 2 files changed, 26 insertions(+), 2 deletions(-) + +diff --git a/drivers/net/bonding/rte_eth_bond_pmd.c b/drivers/net/bonding/rte_eth_bond_pmd.c +index 42e436c..a35422c 100644 +--- a/drivers/net/bonding/rte_eth_bond_pmd.c ++++ b/drivers/net/bonding/rte_eth_bond_pmd.c +@@ -573,6 +573,22 @@ struct client_stats_t { + return nb_recv_pkts; + } + ++static inline int ++bond_ethdev_populate_slave_by_user(const struct rte_mbuf *mbuf, const uint16_t *slaves, ++ int num_slave) ++{ ++ uint16_t i, pid = mbuf->hash.txadapter.reserved2; ++ ++ if (likely(pid == RTE_MBUF_PORT_INVALID)) ++ return -1; ++ ++ for (i = 0; i < num_slave; i++) { ++ if (slaves[i] == pid) ++ return i; ++ } ++ return -1; ++} ++ + static uint16_t + bond_ethdev_tx_burst_round_robin(void *queue, struct rte_mbuf **bufs, + uint16_t nb_pkts) +@@ -605,7 +621,9 @@ struct client_stats_t { + + /* Populate slaves mbuf with which packets are to be sent on it */ + for (i = 0; i < nb_pkts; i++) { +- cslave_idx = (slave_idx + i) % num_of_slaves; ++ cslave_idx = bond_ethdev_populate_slave_by_user(bufs[i], slaves, num_of_slaves); ++ if (likely(cslave_idx < 0)) ++ cslave_idx = (slave_idx + i) % num_of_slaves; + slave_bufs[cslave_idx][(slave_nb_pkts[cslave_idx])++] = bufs[i]; + } + +@@ -1162,7 +1180,11 @@ struct bwg_slave { + + for (i = 0; i < nb_bufs; i++) { + /* Populate slave mbuf arrays with mbufs for that slave. */ +- uint16_t slave_idx = bufs_slave_port_idxs[i]; ++ int slave_idx; ++ ++ slave_idx = bond_ethdev_populate_slave_by_user(bufs[i], slave_port_ids, slave_count); ++ if (likely(slave_idx < 0)) ++ slave_idx = bufs_slave_port_idxs[i]; + + slave_bufs[slave_idx][slave_nb_bufs[slave_idx]++] = bufs[i]; + } +diff --git a/lib/librte_mbuf/rte_mbuf.h b/lib/librte_mbuf/rte_mbuf.h +index c4c9ebf..130b99d 100644 +--- a/lib/librte_mbuf/rte_mbuf.h ++++ b/lib/librte_mbuf/rte_mbuf.h +@@ -589,6 +589,7 @@ static inline struct rte_mbuf *rte_mbuf_raw_alloc(struct rte_mempool *mp) + + if (rte_mempool_get(mp, (void **)&m) < 0) + return NULL; ++ m->hash.txadapter.reserved2 = RTE_MBUF_PORT_INVALID; + __rte_mbuf_raw_sanity_check(m); + return m; + } +@@ -867,6 +868,7 @@ static inline void rte_pktmbuf_reset(struct rte_mbuf *m) + m->vlan_tci_outer = 0; + m->nb_segs = 1; + m->port = RTE_MBUF_PORT_INVALID; ++ m->hash.txadapter.reserved2 = RTE_MBUF_PORT_INVALID; + + m->ol_flags &= EXT_ATTACHED_MBUF; + m->packet_type = 0; +-- +1.8.3.1 + From 72bd6d2c34f9bef30c11ce801e695429984f45b3 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Mon, 5 Aug 2024 11:35:32 +0800 Subject: [PATCH 42/63] lldp: fix strict-aliasing errors when compiling with O3 Signed-off-by: ywc689 --- src/lldp.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/lldp.c b/src/lldp.c index 7edc31a3e..26c4953cb 100644 --- a/src/lldp.c +++ b/src/lldp.c @@ -580,6 +580,7 @@ static int lldp_on_change_ttl(const struct lldp_entry *e) { struct lldp_port *port = e->port; uint16_t ttl; + const void *ptr; /* Lifespan of local lldp caches is not decided by ttl. Actually, they are * updated periodically in every DPVS_LLDP_UPDATE_INTERVAL second. If not updated @@ -588,7 +589,8 @@ static int lldp_on_change_ttl(const struct lldp_entry *e) if (port->neigh == DPVS_LLDP_NODE_LOCAL) return EDPVS_OK; - ttl = rte_be_to_cpu_16(*((uint16_t *)e->value)); + ptr = &e->value[0]; + ttl = rte_be_to_cpu_16(*((uint16_t *)ptr)); if (ttl != port->timeout) { RTE_LOG(INFO, LLDP, "%s: update neigh lldp ttl %u -> %u\n", __func__, port->timeout, ttl); port->timeout = ttl; @@ -762,11 +764,14 @@ static int lldp_dump_sys_cap(const struct lldp_entry *e, char *buf, size_t len) uint16_t capacities, enables; int pos = 0; char tbuf[256]; + const void *ptr; if (e->len != 4) return EDPVS_INVPKT; - capacities = rte_be_to_cpu_16(*((uint16_t *)&e->value[0])); - enables = rte_be_to_cpu_16(*((uint16_t *)&e->value[2])); + ptr = &e->value[0]; + capacities = rte_be_to_cpu_16(*((uint16_t *)ptr)); + ptr = &e->value[2]; + enables = rte_be_to_cpu_16(*((uint16_t *)ptr)); lldp_dump_snprintf(tbuf, pos, "System Capabilities TLV (%d)\n", e->type.type); @@ -832,6 +837,7 @@ static int lldp_local_pdu_mng_addr(const struct netif_port *dev, uint32_t subtyp uint8_t *ptr; struct sockaddr_storage addr; char ifname[IFNAMSIZ]; + uint16_t typlen; ptr = tbuf + 2; *(ptr + 1) = subtype; @@ -884,7 +890,8 @@ static int lldp_local_pdu_mng_addr(const struct netif_port *dev, uint32_t subtyp ptr += 4; /* OID String Length */ *ptr++ = 0; - *((uint16_t *)tbuf) = DPVS_LLDP_TL(LLDP_TYPE_MNG_ADDR, ptr - tbuf - 2); + typlen = DPVS_LLDP_TL(LLDP_TYPE_MNG_ADDR, ptr - tbuf - 2); + rte_memcpy(tbuf, &typlen, 2); if (ptr - tbuf > len) rte_memcpy(buf, tbuf, len); @@ -1625,8 +1632,9 @@ static int lldp_rcv_msg_cb(struct dpvs_msg *msg) portid_t pid, start, end; struct netif_port *dev; struct rte_mbuf *mbuf; + void *msgdata = msg->data; - mbuf = *(struct rte_mbuf **)(msg->data); + mbuf = *(struct rte_mbuf **)msgdata; pid = mbuf->port; netif_bond_port_range(&start, &end); From dcd034a6fd558c8088cd1c969c266d559100aa09 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Mon, 17 Jun 2024 11:36:11 +0800 Subject: [PATCH 43/63] ipvs: add ipset-type blklst to support deny list in network-cidr granularity Signed-off-by: ywc689 --- include/conf/blklst.h | 10 +- include/ipvs/blklst.h | 19 +- src/ipvs/ip_vs_blklst.c | 444 ++++++++++++------ src/ipvs/ip_vs_proto_sctp.c | 4 +- src/ipvs/ip_vs_proto_tcp.c | 4 +- src/ipvs/ip_vs_proto_udp.c | 4 +- src/ipvs/ip_vs_synproxy.c | 4 +- tools/ipvsadm/ipvsadm.c | 85 ++-- .../keepalived/keepalived/check/check_data.c | 17 +- .../keepalived/keepalived/check/ipvswrapper.c | 53 ++- tools/keepalived/keepalived/check/ipwrapper.c | 2 + tools/keepalived/keepalived/check/libipvs.c | 15 +- .../keepalived/include/check_data.h | 2 + 13 files changed, 435 insertions(+), 228 deletions(-) diff --git a/include/conf/blklst.h b/include/conf/blklst.h index 2b2c9a044..84d7be271 100644 --- a/include/conf/blklst.h +++ b/include/conf/blklst.h @@ -24,6 +24,7 @@ #include "inet.h" #include "conf/sockopts.h" +#include "conf/ipset.h" struct dp_vs_blklst_entry { union inet_addr addr; @@ -31,15 +32,14 @@ struct dp_vs_blklst_entry { typedef struct dp_vs_blklst_conf { /* identify service */ - union inet_addr blklst; union inet_addr vaddr; - int af; - uint32_t fwmark; uint16_t vport; uint8_t proto; - uint8_t padding; + uint8_t af; - /* for set */ + /* subject and ipset are mutual exclusive */ + union inet_addr subject; + char ipset[IPSET_MAXNAMELEN]; } dpvs_blklst_t; struct dp_vs_blklst_conf_array { diff --git a/include/ipvs/blklst.h b/include/ipvs/blklst.h index d3326be93..bb9b0cd06 100644 --- a/include/ipvs/blklst.h +++ b/include/ipvs/blklst.h @@ -18,20 +18,25 @@ #ifndef __DPVS_BLKLST_H__ #define __DPVS_BLKLST_H__ #include "conf/common.h" -#include "ipvs/service.h" #include "timer.h" +#include "ipvs/service.h" +#include "ipset/ipset.h" struct blklst_entry { struct list_head list; - int af; - uint8_t proto; - uint16_t vport; + union inet_addr vaddr; - union inet_addr blklst; + uint16_t vport; + uint8_t proto; + uint8_t af; + + union inet_addr subject; + struct ipset *set; + bool dst_match; /* internal use for ipset */ }; -struct blklst_entry *dp_vs_blklst_lookup(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *blklst); +bool dp_vs_blklst_filtered(int af, uint8_t proto, const union inet_addr *vaddr, + uint16_t vport, const union inet_addr *subject, struct rte_mbuf *mbuf); void dp_vs_blklst_flush(struct dp_vs_service *svc); int dp_vs_blklst_init(void); diff --git a/src/ipvs/ip_vs_blklst.c b/src/ipvs/ip_vs_blklst.c index 0afebc6cb..25491a076 100644 --- a/src/ipvs/ip_vs_blklst.c +++ b/src/ipvs/ip_vs_blklst.c @@ -35,86 +35,198 @@ * */ -#define DPVS_BLKLST_TAB_BITS 16 -#define DPVS_BLKLST_TAB_SIZE (1 << DPVS_BLKLST_TAB_BITS) -#define DPVS_BLKLST_TAB_MASK (DPVS_BLKLST_TAB_SIZE - 1) - -#define this_blklst_tab (RTE_PER_LCORE(dp_vs_blklst_tab)) -#define this_num_blklsts (RTE_PER_LCORE(num_blklsts)) +#define DPVS_BLKLST_TAB_BITS 16 +#define DPVS_BLKLST_TAB_SIZE (1 << DPVS_BLKLST_TAB_BITS) +#define DPVS_BLKLST_TAB_MASK (DPVS_BLKLST_TAB_SIZE - 1) +#define this_blklst_tab (RTE_PER_LCORE(dp_vs_blklst_tab)) +#define this_num_blklsts (RTE_PER_LCORE(num_blklsts)) + +#define DPVS_BLKLST_IPSET_TAB_BITS 8 +#define DPVS_BLKLST_IPSET_TAB_SIZE (1 << DPVS_BLKLST_IPSET_TAB_BITS) +#define DPVS_BLKLST_IPSET_TAB_MASK (DPVS_BLKLST_IPSET_TAB_SIZE - 1) +#define this_blklst_ipset_tab (RTE_PER_LCORE(dp_vs_blklst_ipset_tab)) +#define this_num_blklsts_ipset (RTE_PER_LCORE(num_blklsts_ipset)) static RTE_DEFINE_PER_LCORE(struct list_head *, dp_vs_blklst_tab); -static RTE_DEFINE_PER_LCORE(rte_atomic32_t, num_blklsts); +static RTE_DEFINE_PER_LCORE(uint32_t, num_blklsts); + +static RTE_DEFINE_PER_LCORE(struct list_head *, dp_vs_blklst_ipset_tab); +static RTE_DEFINE_PER_LCORE(uint32_t, num_blklsts_ipset); static uint32_t dp_vs_blklst_rnd; +static inline void blklst_fill_conf(const struct blklst_entry *entry, + struct dp_vs_blklst_conf *conf) +{ + memset(conf, 0, sizeof(*conf)); + conf->vaddr = entry->vaddr; + conf->vport = entry->vport; + conf->proto = entry->proto; + conf->af = entry->af; + conf->subject = entry->subject; + if (entry->set) + strncpy(conf->ipset, entry->set->name, sizeof(conf->ipset) - 1); +} + static inline uint32_t blklst_hashkey(const union inet_addr *vaddr, - const union inet_addr *blklst) + uint16_t vport, const union inet_addr *subject, bool ipset) { /* jhash hurts performance, we do not use rte_jhash_2words here */ - return ((rte_be_to_cpu_32(vaddr->in.s_addr) * 31 - + rte_be_to_cpu_32(blklst->in.s_addr)) * 31 - + dp_vs_blklst_rnd) & DPVS_BLKLST_TAB_MASK; + if (ipset) + return ((((vaddr->in.s_addr * 31) ^ vport) * 131) + ^ dp_vs_blklst_rnd) & DPVS_BLKLST_IPSET_TAB_MASK; + + return ((((vaddr->in.s_addr * 31) ^ subject->in.s_addr) * 131) + ^ (vport ^ dp_vs_blklst_rnd)) & DPVS_BLKLST_TAB_MASK; } -struct blklst_entry *dp_vs_blklst_lookup(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *blklst) +static inline struct blklst_entry *dp_vs_blklst_ip_lookup( + int af, uint8_t proto, const union inet_addr *vaddr, + uint16_t vport, const union inet_addr *subject) { unsigned hashkey; - struct blklst_entry *blklst_node; - - hashkey = blklst_hashkey(vaddr, blklst); - list_for_each_entry(blklst_node, &this_blklst_tab[hashkey], list) { - if (blklst_node->af == af && blklst_node->proto == proto && - blklst_node->vport == vport && - inet_addr_equal(af, &blklst_node->vaddr, vaddr) && - inet_addr_equal(af, &blklst_node->blklst, blklst)) - return blklst_node; + struct blklst_entry *entry; + + hashkey = blklst_hashkey(vaddr, vport, subject, false); + list_for_each_entry(entry, &this_blklst_tab[hashkey], list) { + if (entry->af == af && entry->proto == proto && + entry->vport == vport && + inet_addr_equal(af, &entry->vaddr, vaddr) && + inet_addr_equal(af, &entry->subject, subject)) + return entry; } + return NULL; } -static int dp_vs_blklst_add_lcore(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *blklst) +static inline struct blklst_entry *dp_vs_blklst_ipset_lookup( + int af, uint8_t proto, const union inet_addr *vaddr, + uint16_t vport, const char *ipset) { unsigned hashkey; - struct blklst_entry *new, *blklst_node; + struct blklst_entry *entry; - blklst_node = dp_vs_blklst_lookup(af, proto, vaddr, vport, blklst); - if (blklst_node) { - return EDPVS_EXIST; + hashkey = blklst_hashkey(vaddr, vport, NULL, true); + list_for_each_entry(entry, &this_blklst_ipset_tab[hashkey], list) { + if (entry->af == af && entry->proto == proto && entry->vport == vport && + inet_addr_equal(af, &entry->vaddr, vaddr) && + !strncmp(entry->set->name, ipset, sizeof(entry->set->name))) + return entry; + } + + return NULL; +} + +static bool dp_vs_blklst_ip_match_set(int af, uint8_t proto, + const union inet_addr *vaddr, uint16_t vport, + struct rte_mbuf *mbuf) +{ + bool res = false; + unsigned hashkey; + struct blklst_entry *entry; + + hashkey = blklst_hashkey(vaddr, vport, NULL, true); + list_for_each_entry(entry, &this_blklst_ipset_tab[hashkey], list) { + if (entry->af == af && entry->proto == proto && + entry->vport == vport && + inet_addr_equal(af, &entry->vaddr, vaddr)) { + rte_pktmbuf_prepend(mbuf, mbuf->l2_len); + res = elem_in_set(entry->set, mbuf, entry->dst_match); + rte_pktmbuf_adj(mbuf, mbuf->l2_len); + if (res) + break; + } } + return res; +} + +static struct blklst_entry *dp_vs_blklst_lookup(const struct dp_vs_blklst_conf *conf) +{ + struct blklst_entry *entry; - hashkey = blklst_hashkey(vaddr, blklst); + entry = dp_vs_blklst_ip_lookup(conf->af, conf->proto, &conf->vaddr, conf->vport, + &conf->subject); + if (entry) + return entry; + + return dp_vs_blklst_ipset_lookup(conf->af, conf->proto, &conf->vaddr, conf->vport, + conf->ipset); +} + +bool dp_vs_blklst_filtered(int af, uint8_t proto, const union inet_addr *vaddr, + uint16_t vport, const union inet_addr *subject, struct rte_mbuf *mbuf) +{ + if (dp_vs_blklst_ip_lookup(af, proto, vaddr, vport, subject)) + return true; + + return dp_vs_blklst_ip_match_set(af, proto, vaddr, vport, mbuf); +} + +static int dp_vs_blklst_add_lcore(const struct dp_vs_blklst_conf *conf) +{ + unsigned hashkey; + struct blklst_entry *new; + bool is_ipset = conf->ipset[0] != '\0'; + + if (dp_vs_blklst_lookup(conf)) + return EDPVS_EXIST; + + hashkey = blklst_hashkey(&conf->vaddr, conf->vport, &conf->subject, is_ipset); new = rte_zmalloc("new_blklst_entry", sizeof(struct blklst_entry), 0); if (unlikely(new == NULL)) return EDPVS_NOMEM; - new->af = af; - new->proto = proto; - new->vport = vport; - memcpy(&new->vaddr, vaddr, sizeof(union inet_addr)); - memcpy(&new->blklst, blklst, sizeof(union inet_addr)); - list_add(&new->list, &this_blklst_tab[hashkey]); - rte_atomic32_inc(&this_num_blklsts); + new->vaddr = conf->vaddr; + new->vport = conf->vport; + new->proto = conf->proto; + new->af = conf->af; + + if (is_ipset) { + new->set = ipset_get(conf->ipset); + if (!new->set) { + RTE_LOG(ERR, SERVICE, "[%2d] %s: ipset %s not found\n", + rte_lcore_id(), __func__, conf->ipset); + rte_free(new); + return EDPVS_INVAL; + } + // Notes: Reassess it when new ipset types added! + if (!strcmp(new->set->type->name, "hash:ip,port,net") || + !strcmp(new->set->type->name, "hash:ip,port,ip") || + !strcmp(new->set->type->name, "hash:net,port,net")) + new->dst_match = true; + else + new->dst_match = false; + list_add(&new->list, &this_blklst_ipset_tab[hashkey]); + ++this_num_blklsts_ipset; + } else { + new->subject = conf->subject; + list_add(&new->list, &this_blklst_tab[hashkey]); + ++this_num_blklsts; + } return EDPVS_OK; } -static int dp_vs_blklst_del_lcore(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *blklst) +static int dp_vs_blklst_del_lcore(const struct dp_vs_blklst_conf *conf) { - struct blklst_entry *blklst_node; - - blklst_node = dp_vs_blklst_lookup(af, proto, vaddr, vport, blklst); - if (blklst_node != NULL) { - list_del(&blklst_node->list); - rte_free(blklst_node); - rte_atomic32_dec(&this_num_blklsts); - return EDPVS_OK; + struct blklst_entry *entry; + + entry = dp_vs_blklst_lookup(conf); + if (!entry) + return EDPVS_NOTEXIST; + + if (entry->set) { /* ipset entry */ + list_del(&entry->list); + ipset_put(entry->set); + --this_num_blklsts_ipset; + } else { /* ip entry */ + list_del(&entry->list); + --this_num_blklsts; } - return EDPVS_NOTEXIST; + rte_free(entry); + return EDPVS_OK; } static uint32_t blklst_msg_seq(void) @@ -123,42 +235,33 @@ static uint32_t blklst_msg_seq(void) return counter++; } -static int dp_vs_blklst_add(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *blklst) +static int dp_vs_blklst_add(const struct dp_vs_blklst_conf *conf) { lcoreid_t cid = rte_lcore_id(); int err; struct dpvs_msg *msg; - struct dp_vs_blklst_conf cf; if (cid != rte_get_main_lcore()) { RTE_LOG(INFO, SERVICE, "%s must set from master lcore\n", __func__); return EDPVS_NOTSUPP; } - memset(&cf, 0, sizeof(struct dp_vs_blklst_conf)); - memcpy(&(cf.vaddr), vaddr,sizeof(union inet_addr)); - memcpy(&(cf.blklst), blklst, sizeof(union inet_addr)); - cf.af = af; - cf.vport = vport; - cf.proto = proto; - - /*set blklst ip on master lcore*/ - err = dp_vs_blklst_add_lcore(af, proto, vaddr, vport, blklst); + /* master lcore */ + err = dp_vs_blklst_add_lcore(conf); if (err && err != EDPVS_EXIST) { - RTE_LOG(ERR, SERVICE, "[%s] fail to set blklst ip -- %s\n", __func__, dpvs_strerror(err)); + RTE_LOG(ERR, SERVICE, "%s: fail to add blklst entry -- %s\n", __func__, dpvs_strerror(err)); return err; } - /*set blklst ip on all slave lcores*/ + /* slave lcores */ msg = msg_make(MSG_TYPE_BLKLST_ADD, blklst_msg_seq(), DPVS_MSG_MULTICAST, - cid, sizeof(struct dp_vs_blklst_conf), &cf); + cid, sizeof(struct dp_vs_blklst_conf), conf); if (unlikely(!msg)) return EDPVS_NOMEM; err = multicast_msg_send(msg, DPVS_MSG_F_ASYNC, NULL); if (err != EDPVS_OK) { msg_destroy(&msg); - RTE_LOG(INFO, SERVICE, "[%s] fail to send multicast message\n", __func__); + RTE_LOG(INFO, SERVICE, "%s: fail to send multicast message\n", __func__); return err; } msg_destroy(&msg); @@ -166,35 +269,27 @@ static int dp_vs_blklst_add(int af, uint8_t proto, const union inet_addr *vaddr, return EDPVS_OK; } -static int dp_vs_blklst_del(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *blklst) +static int dp_vs_blklst_del(const struct dp_vs_blklst_conf *conf) { lcoreid_t cid = rte_lcore_id(); int err; struct dpvs_msg *msg; - struct dp_vs_blklst_conf cf; if (cid != rte_get_main_lcore()) { RTE_LOG(INFO, SERVICE, "%s must set from master lcore\n", __func__); return EDPVS_NOTSUPP; } - memset(&cf, 0, sizeof(struct dp_vs_blklst_conf)); - memcpy(&(cf.vaddr), vaddr,sizeof(union inet_addr)); - memcpy(&(cf.blklst), blklst, sizeof(union inet_addr)); - cf.af = af; - cf.vport = vport; - cf.proto = proto; - - /*del blklst ip on master lcores*/ - err = dp_vs_blklst_del_lcore(af, proto, vaddr, vport, blklst); + /* master lcore */ + err = dp_vs_blklst_del_lcore(conf); if (err) { + RTE_LOG(ERR, SERVICE, "%s: fail to del blklst entry -- %s\n", __func__, dpvs_strerror(err)); return err; } - /*del blklst ip on all slave lcores*/ + /* slave lcores */ msg = msg_make(MSG_TYPE_BLKLST_DEL, blklst_msg_seq(), DPVS_MSG_MULTICAST, - cid, sizeof(struct dp_vs_blklst_conf), &cf); + cid, sizeof(struct dp_vs_blklst_conf), conf); if (!msg) return EDPVS_NOMEM; err = multicast_msg_send(msg, DPVS_MSG_F_ASYNC, NULL); @@ -212,32 +307,52 @@ void dp_vs_blklst_flush(struct dp_vs_service *svc) { int hash; struct blklst_entry *entry, *next; + struct dp_vs_blklst_conf conf; for (hash = 0; hash < DPVS_BLKLST_TAB_SIZE; hash++) { list_for_each_entry_safe(entry, next, &this_blklst_tab[hash], list) { if (entry->af == svc->af - && entry->vport == svc->port - && entry->proto == svc->proto - && inet_addr_equal(svc->af, &entry->vaddr, &svc->addr)) - dp_vs_blklst_del(svc->af, entry->proto, &entry->vaddr, - entry->vport, &entry->blklst); + && entry->vport == svc->port + && entry->proto == svc->proto + && inet_addr_equal(svc->af, &entry->vaddr, &svc->addr)) { + blklst_fill_conf(entry, &conf); + dp_vs_blklst_del(&conf); + } + } + } + + for (hash = 0; hash < DPVS_BLKLST_IPSET_TAB_SIZE; hash++) { + list_for_each_entry_safe(entry, next, &this_blklst_ipset_tab[hash], list) { + if (entry->af == svc->af + && entry->vport == svc->port + && entry->proto == svc->proto + && inet_addr_equal(svc->af, &entry->vaddr, &svc->addr)) { + blklst_fill_conf(entry, &conf); + dp_vs_blklst_del(&conf); + } } } - return; } static void dp_vs_blklst_flush_all(void) { - struct blklst_entry *entry, *next; int hash; + struct blklst_entry *entry, *next; + struct dp_vs_blklst_conf conf; for (hash = 0; hash < DPVS_BLKLST_TAB_SIZE; hash++) { list_for_each_entry_safe(entry, next, &this_blklst_tab[hash], list) { - dp_vs_blklst_del(entry->af, entry->proto, &entry->vaddr, - entry->vport, &entry->blklst); + blklst_fill_conf(entry, &conf); + dp_vs_blklst_del(&conf); + } + } + + for (hash = 0; hash < DPVS_BLKLST_IPSET_TAB_SIZE; hash++) { + list_for_each_entry_safe(entry, next, &this_blklst_ipset_tab[hash], list) { + blklst_fill_conf(entry, &conf); + dp_vs_blklst_del(&conf); } } - return; } /* @@ -245,40 +360,19 @@ static void dp_vs_blklst_flush_all(void) */ static int blklst_sockopt_set(sockoptid_t opt, const void *conf, size_t size) { - const struct dp_vs_blklst_conf *blklst_conf = conf; - int err; - - if (!conf && size < sizeof(*blklst_conf)) + if (!conf && size < sizeof(struct dp_vs_blklst_conf)) return EDPVS_INVAL; switch (opt) { - case SOCKOPT_SET_BLKLST_ADD: - err = dp_vs_blklst_add(blklst_conf->af, - blklst_conf->proto, &blklst_conf->vaddr, - blklst_conf->vport, &blklst_conf->blklst); - break; - case SOCKOPT_SET_BLKLST_DEL: - err = dp_vs_blklst_del(blklst_conf->af, - blklst_conf->proto, &blklst_conf->vaddr, - blklst_conf->vport, &blklst_conf->blklst); - break; - default: - err = EDPVS_NOTSUPP; - break; + case SOCKOPT_SET_BLKLST_ADD: + return dp_vs_blklst_add(conf); + case SOCKOPT_SET_BLKLST_DEL: + return dp_vs_blklst_del(conf); + default: + return EDPVS_NOTSUPP; } - return err; -} - -static void blklst_fill_conf(struct dp_vs_blklst_conf *cf, - const struct blklst_entry *entry) -{ - memset(cf, 0 ,sizeof(*cf)); - cf->af = entry->af; - cf->vaddr = entry->vaddr; - cf->blklst = entry->blklst; - cf->proto = entry->proto; - cf->vport = entry->vport; + return EDPVS_OK; } static int blklst_sockopt_get(sockoptid_t opt, const void *conf, size_t size, @@ -289,7 +383,7 @@ static int blklst_sockopt_get(sockoptid_t opt, const void *conf, size_t size, size_t naddr, hash; int off = 0; - naddr = rte_atomic32_read(&this_num_blklsts); + naddr = this_num_blklsts + this_num_blklsts_ipset; *outsize = sizeof(struct dp_vs_blklst_conf_array) + naddr * sizeof(struct dp_vs_blklst_conf); *out = rte_calloc(NULL, 1, *outsize, 0); @@ -302,45 +396,52 @@ static int blklst_sockopt_get(sockoptid_t opt, const void *conf, size_t size, list_for_each_entry(entry, &this_blklst_tab[hash], list) { if (off >= naddr) break; - blklst_fill_conf(&array->blklsts[off++], entry); + blklst_fill_conf(entry, &array->blklsts[off++]); + } + } + + for (hash = 0; hash < DPVS_BLKLST_IPSET_TAB_SIZE; hash++) { + list_for_each_entry(entry, &this_blklst_ipset_tab[hash], list) { + if (off >= naddr) + break; + blklst_fill_conf(entry, &array->blklsts[off++]); } } return EDPVS_OK; } - static int blklst_msg_process(bool add, struct dpvs_msg *msg) { - struct dp_vs_blklst_conf *cf; + struct dp_vs_blklst_conf *conf; int err; assert(msg); if (msg->len != sizeof(struct dp_vs_blklst_conf)){ - RTE_LOG(ERR, SERVICE, "%s: bad message.\n", __func__); + RTE_LOG(ERR, SERVICE, "%s: bad message\n", __func__); return EDPVS_INVAL; } - cf = (struct dp_vs_blklst_conf *)msg->data; + conf = (struct dp_vs_blklst_conf *)msg->data; if (add) { - err = dp_vs_blklst_add_lcore(cf->af, cf->proto, &cf->vaddr, cf->vport, &cf->blklst); - if (err && err != EDPVS_EXIST) { - RTE_LOG(ERR, SERVICE, "%s: fail to add blklst: %s.\n", __func__, dpvs_strerror(err)); - } - } - else { - err = dp_vs_blklst_del_lcore(cf->af, cf->proto, &cf->vaddr, cf->vport, &cf->blklst); + err = dp_vs_blklst_add_lcore(conf); + if (err && err != EDPVS_EXIST) + RTE_LOG(ERR, SERVICE, "%s: fail to add blklst: %s\n", __func__, dpvs_strerror(err)); + } else { + err = dp_vs_blklst_del_lcore(conf); + if (err && err != EDPVS_NOTEXIST) + RTE_LOG(ERR, SERVICE, "%s: fail to del blklst: %s\n", __func__, dpvs_strerror(err)); } return err; } -inline static int blklst_add_msg_cb(struct dpvs_msg *msg) +static inline int blklst_add_msg_cb(struct dpvs_msg *msg) { return blklst_msg_process(true, msg); } -inline static int blklst_del_msg_cb(struct dpvs_msg *msg) +static inline int blklst_del_msg_cb(struct dpvs_msg *msg) { return blklst_msg_process(false, msg); } @@ -358,17 +459,29 @@ static struct dpvs_sockopts blklst_sockopts = { static int blklst_lcore_init(void *args) { int i; + if (!rte_lcore_is_enabled(rte_lcore_id())) - return EDPVS_DISABLED; - this_blklst_tab = rte_malloc(NULL, - sizeof(struct list_head) * DPVS_BLKLST_TAB_SIZE, - RTE_CACHE_LINE_SIZE); + return EDPVS_DISABLED; + + this_num_blklsts = 0; + this_num_blklsts_ipset = 0; + + this_blklst_tab = rte_malloc(NULL, sizeof(struct list_head) * + DPVS_BLKLST_TAB_SIZE, RTE_CACHE_LINE_SIZE); if (!this_blklst_tab) return EDPVS_NOMEM; - for (i = 0; i < DPVS_BLKLST_TAB_SIZE; i++) INIT_LIST_HEAD(&this_blklst_tab[i]); + this_blklst_ipset_tab = rte_malloc(NULL, sizeof(struct list_head) * + DPVS_BLKLST_IPSET_TAB_SIZE, RTE_CACHE_LINE_SIZE); + if (!this_blklst_ipset_tab) { + rte_free(this_blklst_tab); + return EDPVS_NOMEM; + } + for (i = 0; i < DPVS_BLKLST_IPSET_TAB_SIZE; i++) + INIT_LIST_HEAD(&this_blklst_ipset_tab[i]); + return EDPVS_OK; } @@ -383,6 +496,12 @@ static int blklst_lcore_term(void *args) rte_free(this_blklst_tab); this_blklst_tab = NULL; } + + if (this_blklst_ipset_tab) { + rte_free(this_blklst_ipset_tab); + this_blklst_ipset_tab = NULL; + } + return EDPVS_OK; } @@ -392,13 +511,11 @@ int dp_vs_blklst_init(void) lcoreid_t cid; struct dpvs_msg_type msg_type; - rte_atomic32_set(&this_num_blklsts, 0); - rte_eal_mp_remote_launch(blklst_lcore_init, NULL, CALL_MAIN); RTE_LCORE_FOREACH_WORKER(cid) { if ((err = rte_eal_wait_lcore(cid)) < 0) { - RTE_LOG(WARNING, SERVICE, "%s: lcore %d: %s.\n", - __func__, cid, dpvs_strerror(err)); + RTE_LOG(WARNING, SERVICE, "[%02d] %s: blklst init failed -- %s\n", + cid, __func__, dpvs_strerror(err)); return err; } } @@ -411,7 +528,8 @@ int dp_vs_blklst_init(void) msg_type.unicast_msg_cb = blklst_add_msg_cb; err = msg_type_mc_register(&msg_type); if (err != EDPVS_OK) { - RTE_LOG(ERR, SERVICE, "%s: fail to register msg.\n", __func__); + RTE_LOG(ERR, SERVICE, "%s: register BLKLST_ADD msg failed -- %s\n", + __func__, dpvs_strerror(err)); return err; } @@ -423,12 +541,17 @@ int dp_vs_blklst_init(void) msg_type.unicast_msg_cb = blklst_del_msg_cb; err = msg_type_mc_register(&msg_type); if (err != EDPVS_OK) { - RTE_LOG(ERR, SERVICE, "%s: fail to register msg.\n", __func__); + RTE_LOG(ERR, SERVICE, "%s: register BLKLST_DEL msg failed -- %s\n", + __func__, dpvs_strerror(err)); return err; } - if ((err = sockopt_register(&blklst_sockopts)) != EDPVS_OK) + if ((err = sockopt_register(&blklst_sockopts)) != EDPVS_OK) { + RTE_LOG(ERR, SERVICE, "%s: register sockopts failed -- %s\n", + __func__, dpvs_strerror(err)); return err; + } + dp_vs_blklst_rnd = (uint32_t)random(); return EDPVS_OK; @@ -438,15 +561,42 @@ int dp_vs_blklst_term(void) { int err; lcoreid_t cid; + struct dpvs_msg_type msg_type; - if ((err = sockopt_unregister(&blklst_sockopts)) != EDPVS_OK) - return err; + if ((err = sockopt_unregister(&blklst_sockopts)) != EDPVS_OK) { + RTE_LOG(WARNING, SERVICE, "%s: unregister sockopts failed -- %s\n", + __func__, dpvs_strerror(err)); + } + + memset(&msg_type, 0, sizeof(struct dpvs_msg_type)); + msg_type.type = MSG_TYPE_BLKLST_DEL; + msg_type.mode = DPVS_MSG_MULTICAST; + msg_type.prio = MSG_PRIO_NORM; + msg_type.cid = rte_lcore_id(); + msg_type.unicast_msg_cb = blklst_del_msg_cb; + err = msg_type_mc_unregister(&msg_type); + if (err != EDPVS_OK) { + RTE_LOG(ERR, SERVICE, "%s: unregister BLKLST_DEL msg failed -- %s\n", + __func__, dpvs_strerror(err)); + } + + memset(&msg_type, 0, sizeof(struct dpvs_msg_type)); + msg_type.type = MSG_TYPE_BLKLST_ADD; + msg_type.mode = DPVS_MSG_MULTICAST; + msg_type.prio = MSG_PRIO_NORM; + msg_type.cid = rte_lcore_id(); + msg_type.unicast_msg_cb = blklst_add_msg_cb; + err = msg_type_mc_unregister(&msg_type); + if (err != EDPVS_OK) { + RTE_LOG(ERR, SERVICE, "%s: unregister BLKLST_ADD msg failed -- %s\n", + __func__, dpvs_strerror(err)); + } rte_eal_mp_remote_launch(blklst_lcore_term, NULL, CALL_MAIN); RTE_LCORE_FOREACH_WORKER(cid) { if ((err = rte_eal_wait_lcore(cid)) < 0) { - RTE_LOG(WARNING, SERVICE, "%s: lcore %d: %s.\n", - __func__, cid, dpvs_strerror(err)); + RTE_LOG(WARNING, SERVICE, "[%02d] %s: blklst termination failed -- %s\n", + cid, __func__, dpvs_strerror(err)); } } diff --git a/src/ipvs/ip_vs_proto_sctp.c b/src/ipvs/ip_vs_proto_sctp.c index 28b067ae1..d46184110 100644 --- a/src/ipvs/ip_vs_proto_sctp.c +++ b/src/ipvs/ip_vs_proto_sctp.c @@ -70,8 +70,8 @@ static struct dp_vs_conn *sctp_conn_lookup(struct dp_vs_proto *proto, if (unlikely(!sch)) return NULL; - if (dp_vs_blklst_lookup(iph->af, iph->proto, &iph->daddr, sh->dest_port, - &iph->saddr)) { + if (dp_vs_blklst_filtered(iph->af, iph->proto, &iph->daddr, sh->dest_port, + &iph->saddr, mbuf)) { *drop = true; return NULL; } diff --git a/src/ipvs/ip_vs_proto_tcp.c b/src/ipvs/ip_vs_proto_tcp.c index 8c7edb501..d28f22cd4 100644 --- a/src/ipvs/ip_vs_proto_tcp.c +++ b/src/ipvs/ip_vs_proto_tcp.c @@ -831,8 +831,8 @@ tcp_conn_lookup(struct dp_vs_proto *proto, const struct dp_vs_iphdr *iph, if (unlikely(!th)) return NULL; - if (dp_vs_blklst_lookup(iph->af, iph->proto, &iph->daddr, - th->dest, &iph->saddr)) { + if (dp_vs_blklst_filtered(iph->af, iph->proto, &iph->daddr, + th->dest, &iph->saddr, mbuf)) { *drop = true; return NULL; } diff --git a/src/ipvs/ip_vs_proto_udp.c b/src/ipvs/ip_vs_proto_udp.c index df58fc924..27275224d 100644 --- a/src/ipvs/ip_vs_proto_udp.c +++ b/src/ipvs/ip_vs_proto_udp.c @@ -235,8 +235,8 @@ udp_conn_lookup(struct dp_vs_proto *proto, if (unlikely(!uh)) return NULL; - if (dp_vs_blklst_lookup(iph->af, iph->proto, &iph->daddr, - uh->dst_port, &iph->saddr)) { + if (dp_vs_blklst_filtered(iph->af, iph->proto, &iph->daddr, + uh->dst_port, &iph->saddr, mbuf)) { *drop = true; return NULL; } diff --git a/src/ipvs/ip_vs_synproxy.c b/src/ipvs/ip_vs_synproxy.c index 5378083b7..815dc9096 100644 --- a/src/ipvs/ip_vs_synproxy.c +++ b/src/ipvs/ip_vs_synproxy.c @@ -758,8 +758,8 @@ int dp_vs_synproxy_syn_rcv(int af, struct rte_mbuf *mbuf, } /* drop packet from blacklist */ - if (dp_vs_blklst_lookup(iph->af, iph->proto, &iph->daddr, - th->dest, &iph->saddr)) { + if (dp_vs_blklst_filtered(iph->af, iph->proto, &iph->daddr, + th->dest, &iph->saddr, mbuf)) { goto syn_rcv_out; } diff --git a/tools/ipvsadm/ipvsadm.c b/tools/ipvsadm/ipvsadm.c index 6e8923d49..69c8e81c4 100644 --- a/tools/ipvsadm/ipvsadm.c +++ b/tools/ipvsadm/ipvsadm.c @@ -902,15 +902,19 @@ parse_options(int argc, char **argv, struct ipvs_command_entry *ce, { dpvs_service_compat_t dpvs_svc; set_option(options,OPT_BLKLST_ADDRESS); - parse = parse_service(optarg, - &dpvs_svc); - if (!(parse & SERVICE_ADDR)) - fail(2, "illegal blacklist address"); - - ce->dpvs_blklst.af = dpvs_svc.af; - ce->dpvs_blklst.blklst = dpvs_svc.addr; + if (!strncmp(optarg, "ipset:", strlen("ipset:"))) { + strncpy(ce->dpvs_blklst.ipset, &optarg[strlen("ipset:")], + sizeof(ce->dpvs_blklst.ipset) - 1); + } else { + parse = parse_service(optarg, &dpvs_svc); + if (parse & SERVICE_ADDR) { + ce->dpvs_blklst.af = dpvs_svc.af; + ce->dpvs_blklst.subject = dpvs_svc.addr; + } else { + fail(2, "illegal blacklist entry format, require [ IP | ipset:NAME ]"); + } + } break; - } case '2': { @@ -1682,9 +1686,9 @@ static void usage_exit(const char *program, const int exit_status) " --add-laddr -P add local address\n" " --del-laddr -Q del local address\n" " --get-laddr -G get local address\n" - " --add-blklst -U add blacklist address\n" - " --del-blklst -V del blacklist address\n" - " --get-blklst -B get blacklist address\n" + " --add-blklst -U add blacklist address or ipset\n" + " --del-blklst -V del blacklist address or ipset\n" + " --get-blklst -B get blacklist address or ipset\n" " --add-whtlst -O add whitelist address\n" " --del-whtlst -Y del whitelist address\n" " --get-whtlst -W get whitelist address\n" @@ -1752,7 +1756,7 @@ static void usage_exit(const char *program, const int exit_status) " UPDOWN:=down_retry,up_confirm,down_wait,inhibit_min-inhibit_max, for example, the default is 1,1,3s,5-3600s\n" " DOWNONLY:=down_retry,down_wait, for example, --dest-check=1,3s\n" " --laddr -z local-ip local IP\n" - " --blklst -k blacklist-ip blacklist IP for specific service\n" + " --blklst -k blacklist-ip specify blacklist ip address or ipset(format: \"ipset:NAME\")\n" " --whtlst -2 whitelist-ip whitelist IP for specific service\n" " --quic itef quic protocol service\n", DEF_SCHED); @@ -2423,17 +2427,17 @@ static int list_all_laddrs(lcoreid_t index) static void list_blklsts_print_title(void) { - printf("%-20s %-8s %-20s\n", - "VIP:VPORT" , + printf("%-8s %-30s %-30s\n", "PROTO" , + "VIP:VPORT" , "BLACKLIST"); } -static void print_service_and_blklsts(struct dp_vs_blklst_conf *blklst) +static void print_service_and_blklsts(const struct dp_vs_blklst_conf *blklst) { - char vip[64], bip[64], port[8], proto[8]; + char subject[64], vip[64], vport[8], proto[8], vip_port[64]; const char *pattern = (blklst->af == AF_INET ? - "%s:%-8s %-8s %-20s\n" : "[%s]:%-8s %-8s %-20s\n"); + "%-8s %-30s %-30s\n" : "%-8s %-30s %-30s\n"); switch (blklst->proto) { case IPPROTO_TCP: @@ -2455,10 +2459,19 @@ static void print_service_and_blklsts(struct dp_vs_blklst_conf *blklst) break; } - snprintf(port, sizeof(port), "%u", ntohs(blklst->vport)); + snprintf(vport, sizeof(vport), "%u", ntohs(blklst->vport)); + inet_ntop(blklst->af, (const void *)&blklst->vaddr, vip, sizeof(vip)); + if (blklst->af == AF_INET6) + snprintf(vip_port, sizeof(vip_port), "[%s]:%s", vip, vport); + else + snprintf(vip_port, sizeof(vip_port), "%s:%s", vip, vport); - printf(pattern, inet_ntop(blklst->af, (const void *)&blklst->vaddr, vip, sizeof(vip)), - port, proto, inet_ntop(blklst->af, (const void *)&blklst->blklst, bip, sizeof(bip))); + if (blklst->ipset[0] == '\0') + inet_ntop(blklst->af, (const void *)&blklst->subject, subject, sizeof(subject)); + else + snprintf(subject, sizeof(subject), "ipset:%s", blklst->ipset); + + printf(pattern, proto, vip_port, subject); } static bool inet_addr_equal(int af, const union inet_addr *a1, const union inet_addr *a2) @@ -2473,9 +2486,19 @@ static bool inet_addr_equal(int af, const union inet_addr *a1, const union inet_ } } +static inline void __list_blklst(int af, const union inet_addr *addr, uint16_t port, + uint16_t protocol, const struct dp_vs_blklst_conf_array *cfarr) { + int i; + for (i = 0; i < cfarr->naddr; i++) { + if (inet_addr_equal(af, addr, (const union inet_addr *) &cfarr->blklsts[i].vaddr) && + port == cfarr->blklsts[i].vport && protocol == cfarr->blklsts[i].proto) { + print_service_and_blklsts(&cfarr->blklsts[i]); + } + } +} + static int list_blklst(int af, const union inet_addr *addr, uint16_t port, uint16_t protocol) { - int i; struct dp_vs_blklst_conf_array *get; if (!(get = dpvs_get_blklsts())) { @@ -2483,14 +2506,9 @@ static int list_blklst(int af, const union inet_addr *addr, uint16_t port, uint1 return -1; } - for (i = 0; i < get->naddr; i++) { - if (inet_addr_equal(af, addr,(const union inet_addr *) &get->blklsts[i].vaddr) && - port == get->blklsts[i].vport && protocol == get->blklsts[i].proto) { - print_service_and_blklsts(&get->blklsts[i]); - } - } - free(get); + __list_blklst(af, addr, port, protocol, get); + free(get); return 0; } @@ -2498,6 +2516,7 @@ static int list_all_blklsts(void) { int i; dpvs_services_front_t* table; + struct dp_vs_blklst_conf_array *barray; table = (dpvs_services_front_t*)malloc(sizeof(dpvs_services_front_t)+sizeof(dpvs_service_compat_t)*g_ipvs_info.num_services); if (!table) { @@ -2513,12 +2532,18 @@ static int list_all_blklsts(void) exit(1); } + if(!(barray = dpvs_get_blklsts())) { + fprintf(stderr, "%s\n", ipvs_strerror(errno)); + exit(1); + } + list_blklsts_print_title(); for (i = 0; i < table->count; i++) { - list_blklst(table->entrytable[i].af, &table->entrytable[i].addr, - table->entrytable[i].port, table->entrytable[i].proto); + __list_blklst(table->entrytable[i].af, &table->entrytable[i].addr, + table->entrytable[i].port, table->entrytable[i].proto, barray); } + free(barray); free(table); return 0; diff --git a/tools/keepalived/keepalived/check/check_data.c b/tools/keepalived/keepalived/check/check_data.c index 95db23d39..b696de6de 100644 --- a/tools/keepalived/keepalived/check/check_data.c +++ b/tools/keepalived/keepalived/check/check_data.c @@ -732,7 +732,9 @@ dump_blklst_entry(FILE *fp, const void *data) { const blklst_addr_entry *blklst_entry = data; - if (blklst_entry->range) + if (!strncmp(blklst_entry->ipset, "ipset:", sizeof("ipset:") - 1)) + conf_write(fp, " IPSET = %s", blklst_entry->ipset); + else if (blklst_entry->range) conf_write(fp, " IP Range = %s-%d" , inet_sockaddrtos(&blklst_entry->addr) , blklst_entry->range); @@ -752,6 +754,7 @@ alloc_blklst_group(char *gname) memcpy(new->gname, gname, size); new->addr_ip = alloc_list(free_blklst_entry, dump_blklst_entry); new->range = alloc_list(free_blklst_entry, dump_blklst_entry); + new->ipset = alloc_list(free_blklst_entry, dump_blklst_entry); list_add(check_data->blklst_group, new); } @@ -761,14 +764,22 @@ alloc_blklst_entry(const vector_t *strvec) { blklst_addr_group *blklst_group = LIST_TAIL_DATA(check_data->blklst_group); blklst_addr_entry *new; + const char *str_entry; new = (blklst_addr_entry *) MALLOC(sizeof (blklst_addr_entry)); + str_entry = strvec_slot(strvec, 0); - inet_stor(vector_slot(strvec, 0), &new->range); + if (!strncmp(str_entry, "ipset:", sizeof("ipset:") - 1)) { + strncpy(new->ipset, &str_entry[sizeof("ipset:")-1], sizeof(new->ipset) - 1); + list_add(blklst_group->ipset, new); + return; + } + + inet_stor(str_entry, &new->range); /* If no range specified, new->range == UINT32_MAX */ if (new->range == UINT32_MAX) new->range = 0; - inet_stosockaddr(vector_slot(strvec, 0), NULL, &new->addr); + inet_stosockaddr(str_entry, NULL, &new->addr); if (!new->range) list_add(blklst_group->addr_ip, new); diff --git a/tools/keepalived/keepalived/check/ipvswrapper.c b/tools/keepalived/keepalived/check/ipvswrapper.c index d3cb4dbd4..5183f11d6 100755 --- a/tools/keepalived/keepalived/check/ipvswrapper.c +++ b/tools/keepalived/keepalived/check/ipvswrapper.c @@ -615,8 +615,8 @@ ipvs_blklst_range_cmd(int cmd, blklst_addr_entry *blklst_entry, dpvs_service_com memset(&blklst_rule, 0, sizeof(dpvs_blklst_t)); blklst_rule.af = blklst_entry->addr.ss_family; if (blklst_entry->addr.ss_family == AF_INET6) { - inet_sockaddrip6(&blklst_entry->addr, &blklst_rule.blklst.in6); - ip = blklst_rule.blklst.in6.s6_addr32[3]; + inet_sockaddrip6(&blklst_entry->addr, &blklst_rule.subject.in6); + ip = blklst_rule.subject.in6.s6_addr32[3]; } else { ip = inet_sockaddrip4(&blklst_entry->addr); } @@ -625,9 +625,9 @@ ipvs_blklst_range_cmd(int cmd, blklst_addr_entry *blklst_entry, dpvs_service_com ((addr_ip >> 24) & 0xFF) <= blklst_entry->range; addr_ip += 0x01000000) { if (blklst_entry->addr.ss_family == AF_INET6) - blklst_rule.blklst.in6.s6_addr32[3] = addr_ip; + blklst_rule.subject.in6.s6_addr32[3] = addr_ip; else - blklst_rule.blklst.in.s_addr = addr_ip; + blklst_rule.subject.in.s_addr = addr_ip; ipvs_talk(cmd, srule, NULL, NULL, NULL, &blklst_rule, NULL, NULL, false); } @@ -649,9 +649,9 @@ ipvs_blklst_group_cmd(int cmd, blklst_addr_group *blklst_group, dpvs_service_com memset(&blklst_rule, 0, sizeof(dpvs_blklst_t)); blklst_rule.af = blklst_entry->addr.ss_family; if (blklst_entry->addr.ss_family == AF_INET6) - inet_sockaddrip6(&blklst_entry->addr, &blklst_rule.blklst.in6); + inet_sockaddrip6(&blklst_entry->addr, &blklst_rule.subject.in6); else - blklst_rule.blklst.in.s_addr = inet_sockaddrip4(&blklst_entry->addr); + blklst_rule.subject.in.s_addr = inet_sockaddrip4(&blklst_entry->addr); ipvs_talk(cmd, srule, NULL, NULL, NULL, &blklst_rule, NULL, NULL, false); } @@ -659,6 +659,13 @@ ipvs_blklst_group_cmd(int cmd, blklst_addr_group *blklst_group, dpvs_service_com LIST_FOREACH(l, blklst_entry, e) { ipvs_blklst_range_cmd(cmd, blklst_entry, srule); } + + l = blklst_group->ipset; + LIST_FOREACH(l, blklst_entry, e) { + memset(&blklst_rule, 0, sizeof(dpvs_blklst_t)); + strncpy(blklst_rule.ipset, blklst_entry->ipset, sizeof(blklst_rule.ipset) - 1); + ipvs_talk(cmd, srule, NULL, NULL, NULL, &blklst_rule, NULL, NULL, false); + } } static void @@ -1281,15 +1288,19 @@ ipvs_rm_bentry_from_vsg(blklst_addr_entry *blklst_entry, whtlst_addr_entry *whtl srule->addr.in.s_addr = ip; if (blklst_entry != NULL) { - if (blklst_entry->range) + if(blklst_entry->ipset[0] != '\0') { + memset(&blklst_rule, 0, sizeof(dpvs_blklst_t)); + strncpy(blklst_rule.ipset, blklst_entry->ipset, sizeof(blklst_rule.ipset) - 1); + ipvs_talk(IP_VS_SO_SET_DELBLKLST, srule, NULL, NULL, NULL, &blklst_rule, NULL, NULL, false); + } else if (blklst_entry->range) { ipvs_blklst_range_cmd(IP_VS_SO_SET_DELBLKLST, blklst_entry, srule); - else { + } else { memset(&blklst_rule, 0, sizeof(dpvs_blklst_t)); blklst_rule.af = blklst_entry->addr.ss_family; if (blklst_entry->addr.ss_family == AF_INET6) - inet_sockaddrip6(&blklst_entry->addr, &blklst_rule.blklst.in6); + inet_sockaddrip6(&blklst_entry->addr, &blklst_rule.subject.in6); else - blklst_rule.blklst.in.s_addr = inet_sockaddrip4(&blklst_entry->addr); + blklst_rule.subject.in.s_addr = inet_sockaddrip4(&blklst_entry->addr); ipvs_talk(IP_VS_SO_SET_DELBLKLST, srule, NULL, NULL, NULL, &blklst_rule, NULL, NULL, false); } } @@ -1319,15 +1330,19 @@ ipvs_rm_bentry_from_vsg(blklst_addr_entry *blklst_entry, whtlst_addr_entry *whtl srule->addr.in.s_addr = addr_ip; if (blklst_entry != NULL) { - if (blklst_entry->range) + if(blklst_entry->ipset[0] != '\0') { + memset(&blklst_rule, 0, sizeof(dpvs_blklst_t)); + strncpy(blklst_rule.ipset, blklst_entry->ipset, sizeof(blklst_rule.ipset) - 1); + ipvs_talk(IP_VS_SO_SET_DELBLKLST, srule, NULL, NULL, NULL, &blklst_rule, NULL, NULL, false); + } else if (blklst_entry->range) { ipvs_blklst_range_cmd(IP_VS_SO_SET_DELBLKLST, blklst_entry, srule); - else { + } else { memset(&blklst_rule, 0, sizeof(dpvs_blklst_t)); blklst_rule.af = blklst_entry->addr.ss_family; if (blklst_entry->addr.ss_family == AF_INET6) - inet_sockaddrip6(&blklst_entry->addr, &blklst_rule.blklst.in6); + inet_sockaddrip6(&blklst_entry->addr, &blklst_rule.subject.in6); else - blklst_rule.blklst.in.s_addr = inet_sockaddrip4(&blklst_entry->addr); + blklst_rule.subject.in.s_addr = inet_sockaddrip4(&blklst_entry->addr); ipvs_talk(IP_VS_SO_SET_DELBLKLST, srule, NULL, NULL, NULL, &blklst_rule, NULL, NULL, false); } @@ -1373,15 +1388,19 @@ ipvs_blklst_remove_entry(virtual_server_t *vs, blklst_addr_entry *blklst_entry) } srule.port = inet_sockaddrport(&vs->addr); - if (blklst_entry->range) { + if(blklst_entry->ipset[0] != '\0') { + memset(&blklst_rule, 0, sizeof(dpvs_blklst_t)); + strncpy(blklst_rule.ipset, blklst_entry->ipset, sizeof(blklst_rule.ipset) - 1); + ipvs_talk(IP_VS_SO_SET_DELBLKLST, &srule, NULL, NULL, NULL, &blklst_rule, NULL, NULL, false); + } else if (blklst_entry->range) { ipvs_blklst_range_cmd(IP_VS_SO_SET_DELBLKLST, blklst_entry, &srule); } else { memset(&blklst_rule, 0, sizeof(dpvs_blklst_t)); blklst_rule.af = blklst_entry->addr.ss_family; if (blklst_entry->addr.ss_family == AF_INET6) - inet_sockaddrip6(&blklst_entry->addr, &blklst_rule.blklst.in6); + inet_sockaddrip6(&blklst_entry->addr, &blklst_rule.subject.in6); else - blklst_rule.blklst.in.s_addr = inet_sockaddrip4(&blklst_entry->addr); + blklst_rule.subject.in.s_addr = inet_sockaddrip4(&blklst_entry->addr); ipvs_talk(IP_VS_SO_SET_DELBLKLST, &srule, NULL, NULL, NULL, &blklst_rule, NULL, NULL, false); } diff --git a/tools/keepalived/keepalived/check/ipwrapper.c b/tools/keepalived/keepalived/check/ipwrapper.c index 7bc45d1c2..1f8706a51 100755 --- a/tools/keepalived/keepalived/check/ipwrapper.c +++ b/tools/keepalived/keepalived/check/ipwrapper.c @@ -1253,6 +1253,8 @@ clear_diff_blklst(virtual_server_t * old_vs, virtual_server_t * new_vs) return 0; if (!clear_diff_blklst_entry(old->range, new->range, old_vs)) return 0; + if (!clear_diff_blklst_entry(old->ipset, new->ipset, old_vs)) + return 0; return 1; } diff --git a/tools/keepalived/keepalived/check/libipvs.c b/tools/keepalived/keepalived/check/libipvs.c index 849ac51ad..8d9096795 100644 --- a/tools/keepalived/keepalived/check/libipvs.c +++ b/tools/keepalived/keepalived/check/libipvs.c @@ -299,17 +299,10 @@ int dpvs_del_laddr(dpvs_service_compat_t *svc, dpvs_laddr_table_t *laddr) /*for black list*/ static void dpvs_fill_blklst_conf(dpvs_service_compat_t *svc, dpvs_blklst_t *blklst) { - blklst->af = svc->af; - blklst->proto = svc->proto; - blklst->vport = svc->port; - blklst->fwmark = svc->fwmark; - if (svc->af == AF_INET) { - blklst->vaddr.in = svc->addr.in; - } else { - blklst->vaddr.in6 = svc->addr.in6; - } - - return; + blklst->af = svc->af; + blklst->proto = svc->proto; + blklst->vport = svc->port; + blklst->vaddr = svc->addr; } int dpvs_add_blklst(dpvs_service_compat_t* svc, dpvs_blklst_t *blklst) diff --git a/tools/keepalived/keepalived/include/check_data.h b/tools/keepalived/keepalived/include/check_data.h index 14650b644..28aec2258 100644 --- a/tools/keepalived/keepalived/include/check_data.h +++ b/tools/keepalived/keepalived/include/check_data.h @@ -132,6 +132,7 @@ typedef struct _local_addr_group { typedef struct _blklst_addr_entry { struct sockaddr_storage addr; uint32_t range; + char ipset[IPSET_MAXNAMELEN]; } blklst_addr_entry; @@ -139,6 +140,7 @@ typedef struct _blklst_addr_group { char *gname; list addr_ip; list range; + list ipset; } blklst_addr_group; /* whitelist ip group*/ From dc1cec805a70252d5152fc978eb4f198381ae77d Mon Sep 17 00:00:00 2001 From: ywc689 Date: Tue, 18 Jun 2024 20:15:07 +0800 Subject: [PATCH 44/63] ipvs: add ipset-type whtlst to support allow list in network-cidr granularity Signed-off-by: ywc689 --- include/conf/whtlst.h | 11 +- include/ipvs/whtlst.h | 15 +- src/ipvs/ip_vs_core.c | 3 +- src/ipvs/ip_vs_proto_sctp.c | 4 +- src/ipvs/ip_vs_proto_tcp.c | 3 +- src/ipvs/ip_vs_proto_udp.c | 4 +- src/ipvs/ip_vs_synproxy.c | 3 +- src/ipvs/ip_vs_whtlst.c | 493 ++++++++++++------ tools/ipvsadm/ipvsadm.c | 96 ++-- .../keepalived/keepalived/check/check_data.c | 48 +- .../keepalived/keepalived/check/ipvswrapper.c | 58 ++- tools/keepalived/keepalived/check/ipwrapper.c | 40 +- tools/keepalived/keepalived/check/libipvs.c | 9 +- .../keepalived/include/check_data.h | 2 + 14 files changed, 513 insertions(+), 276 deletions(-) diff --git a/include/conf/whtlst.h b/include/conf/whtlst.h index 8da9f4d57..4edc8880b 100644 --- a/include/conf/whtlst.h +++ b/include/conf/whtlst.h @@ -23,21 +23,22 @@ #define __DPVS_WHTLST_CONF_H__ #include "inet.h" #include "conf/sockopts.h" +#include "conf/ipset.h" + struct dp_vs_whtlst_entry { union inet_addr addr; }; typedef struct dp_vs_whtlst_conf { /* identify service */ - union inet_addr whtlst; union inet_addr vaddr; - int af; - uint32_t fwmark; uint16_t vport; uint8_t proto; - uint8_t padding; + uint8_t af; - /* for set */ + /* subject and ipset are mutual exclusive */ + union inet_addr subject; + char ipset[IPSET_MAXNAMELEN]; } dpvs_whtlst_t; struct dp_vs_whtlst_conf_array { diff --git a/include/ipvs/whtlst.h b/include/ipvs/whtlst.h index 2288d5f96..d447bc5cc 100644 --- a/include/ipvs/whtlst.h +++ b/include/ipvs/whtlst.h @@ -19,20 +19,23 @@ #define __DPVS_WHTLST_H__ #include "conf/common.h" #include "ipvs/service.h" +#include "ipset/ipset.h" struct whtlst_entry { struct list_head list; - int af; + union inet_addr vaddr; uint16_t vport; uint8_t proto; - union inet_addr whtlst; + uint8_t af; + + union inet_addr subject; + struct ipset *set; + bool dst_match; /* internal use of ipset */ }; -struct whtlst_entry *dp_vs_whtlst_lookup(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *whtlst); -bool dp_vs_whtlst_allow(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *whtlst); +bool dp_vs_whtlst_filtered(int af, uint8_t proto, const union inet_addr *vaddr, + uint16_t vport, const union inet_addr *subject, struct rte_mbuf *mbuf); void dp_vs_whtlst_flush(struct dp_vs_service *svc); int dp_vs_whtlst_init(void); diff --git a/src/ipvs/ip_vs_core.c b/src/ipvs/ip_vs_core.c index b92e98496..f01f41ba0 100644 --- a/src/ipvs/ip_vs_core.c +++ b/src/ipvs/ip_vs_core.c @@ -984,9 +984,8 @@ static int __dp_vs_in(void *priv, struct rte_mbuf *mbuf, /* packet belongs to existing connection ? */ conn = prot->conn_lookup(prot, &iph, mbuf, &dir, false, &drop, &peer_cid); - if (unlikely(drop)) { - RTE_LOG(DEBUG, IPVS, "%s: deny ip try to visit.\n", __func__); + RTE_LOG(DEBUG, IPVS, "%s: packet dropped by ipvs acl\n", __func__); return INET_DROP; } diff --git a/src/ipvs/ip_vs_proto_sctp.c b/src/ipvs/ip_vs_proto_sctp.c index d46184110..be2330e5f 100644 --- a/src/ipvs/ip_vs_proto_sctp.c +++ b/src/ipvs/ip_vs_proto_sctp.c @@ -76,8 +76,8 @@ static struct dp_vs_conn *sctp_conn_lookup(struct dp_vs_proto *proto, return NULL; } - if (!dp_vs_whtlst_allow(iph->af, iph->proto, &iph->daddr, sh->dest_port, - &iph->saddr)) { + if (dp_vs_whtlst_filtered(iph->af, iph->proto, &iph->daddr, sh->dest_port, + &iph->saddr, mbuf)) { *drop = true; return NULL; } diff --git a/src/ipvs/ip_vs_proto_tcp.c b/src/ipvs/ip_vs_proto_tcp.c index d28f22cd4..b10aedd49 100644 --- a/src/ipvs/ip_vs_proto_tcp.c +++ b/src/ipvs/ip_vs_proto_tcp.c @@ -837,7 +837,8 @@ tcp_conn_lookup(struct dp_vs_proto *proto, const struct dp_vs_iphdr *iph, return NULL; } - if (!dp_vs_whtlst_allow(iph->af, iph->proto, &iph->daddr, th->dest, &iph->saddr)) { + if (dp_vs_whtlst_filtered(iph->af, iph->proto, &iph->daddr, + th->dest, &iph->saddr, mbuf)) { *drop = true; return NULL; } diff --git a/src/ipvs/ip_vs_proto_udp.c b/src/ipvs/ip_vs_proto_udp.c index 27275224d..b0fa040f2 100644 --- a/src/ipvs/ip_vs_proto_udp.c +++ b/src/ipvs/ip_vs_proto_udp.c @@ -241,8 +241,8 @@ udp_conn_lookup(struct dp_vs_proto *proto, return NULL; } - if (!dp_vs_whtlst_allow(iph->af, iph->proto, &iph->daddr, - uh->dst_port, &iph->saddr)) { + if (dp_vs_whtlst_filtered(iph->af, iph->proto, &iph->daddr, + uh->dst_port, &iph->saddr, mbuf)) { *drop = true; return NULL; } diff --git a/src/ipvs/ip_vs_synproxy.c b/src/ipvs/ip_vs_synproxy.c index 815dc9096..305b84f40 100644 --- a/src/ipvs/ip_vs_synproxy.c +++ b/src/ipvs/ip_vs_synproxy.c @@ -764,7 +764,8 @@ int dp_vs_synproxy_syn_rcv(int af, struct rte_mbuf *mbuf, } /* drop packet if not in whitelist */ - if (!dp_vs_whtlst_allow(iph->af, iph->proto, &iph->daddr, th->dest, &iph->saddr)) { + if (dp_vs_whtlst_filtered(iph->af, iph->proto, &iph->daddr, + th->dest, &iph->saddr, mbuf)) { goto syn_rcv_out; } } else { diff --git a/src/ipvs/ip_vs_whtlst.c b/src/ipvs/ip_vs_whtlst.c index bea11c922..672077456 100644 --- a/src/ipvs/ip_vs_whtlst.c +++ b/src/ipvs/ip_vs_whtlst.c @@ -38,103 +38,257 @@ #define DPVS_WHTLST_TAB_BITS 16 #define DPVS_WHTLST_TAB_SIZE (1 << DPVS_WHTLST_TAB_BITS) #define DPVS_WHTLST_TAB_MASK (DPVS_WHTLST_TAB_SIZE - 1) - #define this_whtlst_tab (RTE_PER_LCORE(dp_vs_whtlst_tab)) #define this_num_whtlsts (RTE_PER_LCORE(num_whtlsts)) +#define DPVS_WHTLST_IPSET_TAB_BITS 8 +#define DPVS_WHTLST_IPSET_TAB_SIZE (1 << DPVS_WHTLST_IPSET_TAB_BITS) +#define DPVS_WHTLST_IPSET_TAB_MASK (DPVS_WHTLST_IPSET_TAB_SIZE - 1) +#define this_whtlst_ipset_tab (RTE_PER_LCORE(dp_vs_whtlst_ipset_tab)) +#define this_num_whtlsts_ipset (RTE_PER_LCORE(num_whtlsts_ipset)) + static RTE_DEFINE_PER_LCORE(struct list_head *, dp_vs_whtlst_tab); -static RTE_DEFINE_PER_LCORE(rte_atomic32_t, num_whtlsts); +static RTE_DEFINE_PER_LCORE(uint32_t, num_whtlsts); + +static RTE_DEFINE_PER_LCORE(struct list_head *, dp_vs_whtlst_ipset_tab); +static RTE_DEFINE_PER_LCORE(uint32_t, num_whtlsts_ipset); static uint32_t dp_vs_whtlst_rnd; -static inline uint32_t whtlst_hashkey(const uint8_t proto, const union inet_addr *vaddr, const uint16_t vport) +static inline void whtlst_fill_conf(const struct whtlst_entry *entry, + struct dp_vs_whtlst_conf *conf) +{ + memset(conf, 0, sizeof(*conf)); + conf->vaddr = entry->vaddr; + conf->vport = entry->vport; + conf->proto = entry->proto; + conf->af = entry->af; + conf->subject = entry->subject; + if (entry->set) + strncpy(conf->ipset, entry->set->name, sizeof(conf->ipset) - 1); +} + +static inline uint32_t whtlst_hashkey(const union inet_addr *vaddr, + uint16_t vport, bool ipset) { /* jhash hurts performance, we do not use rte_jhash_2words here */ - return (((rte_be_to_cpu_16(proto) * 7 - + rte_be_to_cpu_32(vaddr->in.s_addr)) * 31 - + rte_be_to_cpu_16(vport)) * 15 - + dp_vs_whtlst_rnd) & DPVS_WHTLST_TAB_MASK; + if (ipset) + return ((((vaddr->in.s_addr * 31) ^ vport) * 131) + ^ dp_vs_whtlst_rnd) & DPVS_WHTLST_IPSET_TAB_MASK; + + return ((((vaddr->in.s_addr * 31) ^ vport) * 131) + ^ dp_vs_whtlst_rnd) & DPVS_WHTLST_TAB_MASK; } -struct whtlst_entry *dp_vs_whtlst_lookup(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *whtlst) +static inline struct whtlst_entry *dp_vs_whtlst_ip_lookup( + int af, uint8_t proto, const union inet_addr *vaddr, + uint16_t vport, const union inet_addr *subject) { unsigned hashkey; - struct whtlst_entry *whtlst_node; - - hashkey = whtlst_hashkey(proto, vaddr, vport); - list_for_each_entry(whtlst_node, &this_whtlst_tab[hashkey], list){ - if (whtlst_node->af == af && whtlst_node->proto == proto && - whtlst_node->vport == vport && - inet_addr_equal(af, &whtlst_node->vaddr, vaddr) && - inet_addr_equal(af, &whtlst_node->whtlst, whtlst)) - return whtlst_node; + struct whtlst_entry *entry; + + hashkey = whtlst_hashkey(vaddr, vport, false); + list_for_each_entry(entry, &this_whtlst_tab[hashkey], list){ + if (entry->af == af && entry->proto == proto && + entry->vport == vport && + inet_addr_equal(af, &entry->vaddr, vaddr) && + inet_addr_equal(af, &entry->subject, subject)) + return entry; } return NULL; } -bool dp_vs_whtlst_allow(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *whtlst) +static inline struct whtlst_entry *dp_vs_whtlst_ipset_lookup( + int af, uint8_t proto, const union inet_addr *vaddr, + uint16_t vport, const char *ipset) { unsigned hashkey; - struct whtlst_entry *whtlst_node; + struct whtlst_entry *entry; + + hashkey = whtlst_hashkey(vaddr, vport, true); + list_for_each_entry(entry, &this_whtlst_ipset_tab[hashkey], list) { + if (entry->af == af && entry->proto == proto && entry->vport == vport && + inet_addr_equal(af, &entry->vaddr, vaddr) && + !strncmp(entry->set->name, ipset, sizeof(entry->set->name))) + return entry; + } - hashkey = whtlst_hashkey(proto, vaddr, vport); + return NULL; +} + +static struct whtlst_entry *dp_vs_whtlst_lookup(const struct dp_vs_whtlst_conf *conf) +{ + struct whtlst_entry *entry; + + entry = dp_vs_whtlst_ip_lookup(conf->af, conf->proto, &conf->vaddr, conf->vport, + &conf->subject); + if (entry) + return entry; - if (&this_whtlst_tab[hashkey] == NULL || list_empty(&this_whtlst_tab[hashkey])) { - return true; + return dp_vs_whtlst_ipset_lookup(conf->af, conf->proto, &conf->vaddr, conf->vport, + conf->ipset); +} + +enum dpvs_whtlst_result { + DPVS_WHTLST_UNSET = 1, + DPVS_WHTLST_DENIED = 2, + DPVS_WHTLST_ALLOWED = 4, +}; + +static inline enum dpvs_whtlst_result dp_vs_whtlst_iplist_acl ( + int af, uint8_t proto, const union inet_addr *vaddr, + uint16_t vport, const union inet_addr *subject) +{ + bool set, hit; + unsigned hashkey; + struct whtlst_entry *entry; + + set = false; + hit = false; + + hashkey = whtlst_hashkey(vaddr, vport, false); + list_for_each_entry(entry, &this_whtlst_tab[hashkey], list){ + if (entry->af == af && entry->proto == proto && + entry->vport == vport && + inet_addr_equal(af, &entry->vaddr, vaddr)) { + set = true; + hit = inet_addr_equal(af, &entry->subject, subject); + if (hit) + break; + } } - list_for_each_entry(whtlst_node, &this_whtlst_tab[hashkey], list){ - if (whtlst_node->af == af && whtlst_node->proto == proto && - whtlst_node->vport == vport && - inet_addr_equal(af, &whtlst_node->vaddr, vaddr) && - inet_addr_equal(af, &whtlst_node->whtlst, whtlst)) - return true; + + if (!set) + return DPVS_WHTLST_UNSET; + if (hit) + return DPVS_WHTLST_ALLOWED; + return DPVS_WHTLST_DENIED; +} + +static enum dpvs_whtlst_result dp_vs_whtlst_ipset_acl(int af, uint8_t proto, + const union inet_addr *vaddr, uint16_t vport, struct rte_mbuf *mbuf) +{ + bool set, hit; + unsigned hashkey; + struct whtlst_entry *entry; + + set = false; + hit = false; + + hashkey = whtlst_hashkey(vaddr, vport, true); + list_for_each_entry(entry, &this_whtlst_ipset_tab[hashkey], list) { + if (entry->af == af && entry->proto == proto && + entry->vport == vport && + inet_addr_equal(af, &entry->vaddr, vaddr)) { + set = true; + rte_pktmbuf_prepend(mbuf, mbuf->l2_len); + hit = elem_in_set(entry->set, mbuf, entry->dst_match); + rte_pktmbuf_adj(mbuf, mbuf->l2_len); + if (hit) + break; + } } - return false; + if (!set) + return DPVS_WHTLST_UNSET; + if (hit) + return DPVS_WHTLST_ALLOWED; + return DPVS_WHTLST_DENIED; +} + +bool dp_vs_whtlst_filtered(int af, uint8_t proto, const union inet_addr *vaddr, + uint16_t vport, const union inet_addr *subject, struct rte_mbuf *mbuf) +{ + /* + * The tri-state combinations for ipset and iplist: + * ---------------------------------------------------- + * ipset\iplist | UNSET ALLOWED DENIED + * -------------|-------------------------------------- + * UNSET | Accept Accept Reject + * ALLOWED | Accept Accept Accept + * DENIED | Reject Accept Reject + * ---------------------------------------------------- + * + * Notes: + * - Once attached to service, the empty ipset whtlst entry denies all. + * The UNSET means no matched entries attached to the service. + * - If a client is allowed in either iplist or ipset, but denied in + * the other, then the client is allowed. + */ + enum dpvs_whtlst_result res1, res2; + + res1 = dp_vs_whtlst_iplist_acl(af, proto, vaddr, vport, subject); + res2 = dp_vs_whtlst_ipset_acl(af, proto, vaddr, vport, mbuf); + + return !((DPVS_WHTLST_ALLOWED == res1) || ( DPVS_WHTLST_ALLOWED == res2) + || ((DPVS_WHTLST_UNSET == res1) && (DPVS_WHTLST_UNSET == res2))); } -static int dp_vs_whtlst_add_lcore(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *whtlst) +static int dp_vs_whtlst_add_lcore(const struct dp_vs_whtlst_conf *conf) { unsigned hashkey; - struct whtlst_entry *new, *whtlst_node; - whtlst_node = dp_vs_whtlst_lookup(af, proto, vaddr, vport, whtlst); - if (whtlst_node) { + struct whtlst_entry *new; + bool is_ipset = conf->ipset[0] != '\0'; + + if (dp_vs_whtlst_lookup(conf)) return EDPVS_EXIST; - } - hashkey = whtlst_hashkey(proto, vaddr, vport); + hashkey = whtlst_hashkey(&conf->vaddr, conf->vport, is_ipset); new = rte_zmalloc("new_whtlst_entry", sizeof(struct whtlst_entry), 0); if (unlikely(new == NULL)) return EDPVS_NOMEM; - new->af = af; - new->vport = vport; - new->proto = proto; - memcpy(&new->vaddr, vaddr, sizeof(union inet_addr)); - memcpy(&new->whtlst, whtlst, sizeof(union inet_addr)); - list_add(&new->list, &this_whtlst_tab[hashkey]); - rte_atomic32_inc(&this_num_whtlsts); + new->vaddr = conf->vaddr; + new->vport = conf->vport; + new->proto = conf->proto; + new->af = conf->af; + + if (is_ipset) { + new->set = ipset_get(conf->ipset); + if (!new->set) { + RTE_LOG(ERR, SERVICE, "[%2d] %s: ipset %s not found\n", + rte_lcore_id(), __func__, conf->ipset); + rte_free(new); + return EDPVS_INVAL; + } + // Notes: Reassess it when new ipset types added! + if (!strcmp(new->set->type->name, "hash:ip,port,net") || + !strcmp(new->set->type->name, "hash:ip,port,ip") || + !strcmp(new->set->type->name, "hash:net,port,net")) + new->dst_match = true; + else + new->dst_match = false; + list_add(&new->list, &this_whtlst_ipset_tab[hashkey]); + ++this_num_whtlsts_ipset; + } else { + new->subject = conf->subject; + list_add(&new->list, &this_whtlst_tab[hashkey]); + ++this_num_whtlsts; + } return EDPVS_OK; } -static int dp_vs_whtlst_del_lcore(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *whtlst) +static int dp_vs_whtlst_del_lcore(const struct dp_vs_whtlst_conf *conf) { - struct whtlst_entry *whtlst_node; - - whtlst_node = dp_vs_whtlst_lookup(af, proto, vaddr, vport, whtlst); - if (whtlst_node != NULL) { - list_del(&whtlst_node->list); - rte_free(whtlst_node); - rte_atomic32_dec(&this_num_whtlsts); - return EDPVS_OK; + struct whtlst_entry *entry; + + entry = dp_vs_whtlst_lookup(conf); + if (!entry) + return EDPVS_NOTEXIST; + + if (entry->set) { /* ipset entry */ + list_del(&entry->list); + ipset_put(entry->set); + --this_num_whtlsts_ipset; + } else { + list_del(&entry->list); + --this_num_whtlsts; } - return EDPVS_NOTEXIST; + + rte_free(entry); + return EDPVS_OK; } static uint32_t whtlst_msg_seq(void) @@ -143,42 +297,33 @@ static uint32_t whtlst_msg_seq(void) return counter++; } -static int dp_vs_whtlst_add(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *whtlst) +static int dp_vs_whtlst_add(const struct dp_vs_whtlst_conf *conf) { lcoreid_t cid = rte_lcore_id(); int err; struct dpvs_msg *msg; - struct dp_vs_whtlst_conf cf; if (cid != rte_get_main_lcore()) { - RTE_LOG(INFO, SERVICE, "[%s] must set from master lcore\n", __func__); + RTE_LOG(INFO, SERVICE, "%s must set from master lcore\n", __func__); return EDPVS_NOTSUPP; } - memset(&cf, 0, sizeof(struct dp_vs_whtlst_conf)); - memcpy(&(cf.vaddr), vaddr, sizeof(union inet_addr)); - memcpy(&(cf.whtlst), whtlst, sizeof(union inet_addr)); - cf.af = af; - cf.vport = vport; - cf.proto = proto; - - /*set whtlst ip on master lcore*/ - err = dp_vs_whtlst_add_lcore(af, proto, vaddr, vport, whtlst); + /* master lcore */ + err = dp_vs_whtlst_add_lcore(conf); if (err && err != EDPVS_EXIST) { - RTE_LOG(ERR, SERVICE, "[%s] fail to set whtlst ip\n", __func__); + RTE_LOG(ERR, SERVICE, "%s: fail to add whtlst entry -- %s\n", __func__, dpvs_strerror(err)); return err; } - /*set whtlst ip on all slave lcores*/ + /* slave lcores */ msg = msg_make(MSG_TYPE_WHTLST_ADD, whtlst_msg_seq(), DPVS_MSG_MULTICAST, - cid, sizeof(struct dp_vs_whtlst_conf), &cf); + cid, sizeof(struct dp_vs_whtlst_conf), conf); if (!msg) return EDPVS_NOMEM; err = multicast_msg_send(msg, DPVS_MSG_F_ASYNC, NULL); if (err != EDPVS_OK) { msg_destroy(&msg); - RTE_LOG(INFO, SERVICE, "[%s] fail to send multicast message\n", __func__); + RTE_LOG(INFO, SERVICE, "%s: fail to send multicast message\n", __func__); return err; } msg_destroy(&msg); @@ -186,41 +331,33 @@ static int dp_vs_whtlst_add(int af, uint8_t proto, const union inet_addr *vaddr, return EDPVS_OK; } -static int dp_vs_whtlst_del(int af, uint8_t proto, const union inet_addr *vaddr, - uint16_t vport, const union inet_addr *whtlst) +static int dp_vs_whtlst_del(const struct dp_vs_whtlst_conf *conf) { lcoreid_t cid = rte_lcore_id(); int err; struct dpvs_msg *msg; - struct dp_vs_whtlst_conf cf; if (cid != rte_get_main_lcore()) { - RTE_LOG(INFO, SERVICE, "[%s] must set from master lcore\n", __func__); + RTE_LOG(INFO, SERVICE, "%s must set from master lcore\n", __func__); return EDPVS_NOTSUPP; } - memset(&cf, 0, sizeof(struct dp_vs_whtlst_conf)); - memcpy(&(cf.vaddr), vaddr, sizeof(union inet_addr)); - memcpy(&(cf.whtlst), whtlst, sizeof(union inet_addr)); - cf.af = af; - cf.vport = vport; - cf.proto = proto; - - /*del whtlst ip on master lcores*/ - err = dp_vs_whtlst_del_lcore(af, proto, vaddr, vport, whtlst); + /* master lcore */ + err = dp_vs_whtlst_del_lcore(conf); if (err) { + RTE_LOG(ERR, SERVICE, "%s: fail to del whtlst entry -- %s\n", __func__, dpvs_strerror(err)); return err; } - /*del whtlst ip on all slave lcores*/ + /* slave lcores */ msg = msg_make(MSG_TYPE_WHTLST_DEL, whtlst_msg_seq(), DPVS_MSG_MULTICAST, - cid, sizeof(struct dp_vs_whtlst_conf), &cf); + cid, sizeof(struct dp_vs_whtlst_conf), conf); if (!msg) return EDPVS_NOMEM; err = multicast_msg_send(msg, DPVS_MSG_F_ASYNC, NULL); if (err != EDPVS_OK) { msg_destroy(&msg); - RTE_LOG(INFO, SERVICE, "[%s] fail to send multicast message\n", __func__); + RTE_LOG(INFO, SERVICE, "%s: fail to send multicast message\n", __func__); return err; } msg_destroy(&msg); @@ -228,36 +365,56 @@ static int dp_vs_whtlst_del(int af, uint8_t proto, const union inet_addr *vaddr, return EDPVS_OK; } -void dp_vs_whtlst_flush(struct dp_vs_service *svc) +void dp_vs_whtlst_flush(struct dp_vs_service *svc) { - struct whtlst_entry *entry, *next; int hash; + struct whtlst_entry *entry, *next; + struct dp_vs_whtlst_conf conf; for (hash = 0; hash < DPVS_WHTLST_TAB_SIZE; hash++) { list_for_each_entry_safe(entry, next, &this_whtlst_tab[hash], list) { if (entry->af == svc->af - && entry->vport == svc->port - && entry->proto == svc->proto - && inet_addr_equal(svc->af, &entry->vaddr, &svc->addr)) - dp_vs_whtlst_del(svc->af, entry->proto, &entry->vaddr, - entry->vport, &entry->whtlst); + && entry->vport == svc->port + && entry->proto == svc->proto + && inet_addr_equal(svc->af, &entry->vaddr, &svc->addr)) { + whtlst_fill_conf(entry, &conf); + dp_vs_whtlst_del(&conf); + } + } + } + + for (hash = 0; hash < DPVS_WHTLST_IPSET_TAB_SIZE; hash++) { + list_for_each_entry_safe(entry, next, &this_whtlst_ipset_tab[hash], list) { + if (entry->af == svc->af + && entry->vport == svc->port + && entry->proto == svc->proto + && inet_addr_equal(svc->af, &entry->vaddr, &svc->addr)) { + whtlst_fill_conf(entry, &conf); + dp_vs_whtlst_del(&conf); + } } } - return; } static void dp_vs_whtlst_flush_all(void) { - struct whtlst_entry *entry, *next; int hash; + struct whtlst_entry *entry, *next; + struct dp_vs_whtlst_conf conf; for (hash = 0; hash < DPVS_WHTLST_TAB_SIZE; hash++) { list_for_each_entry_safe(entry, next, &this_whtlst_tab[hash], list) { - dp_vs_whtlst_del(entry->af, entry->proto, &entry->vaddr, - entry->vport, &entry->whtlst); + whtlst_fill_conf(entry, &conf); + dp_vs_whtlst_del(&conf); + } + } + + for (hash = 0; hash < DPVS_WHTLST_IPSET_TAB_SIZE; hash++) { + list_for_each_entry_safe(entry, next, &this_whtlst_ipset_tab[hash], list) { + whtlst_fill_conf(entry, &conf); + dp_vs_whtlst_del(&conf); } } - return; } /* @@ -265,40 +422,19 @@ static void dp_vs_whtlst_flush_all(void) */ static int whtlst_sockopt_set(sockoptid_t opt, const void *conf, size_t size) { - const struct dp_vs_whtlst_conf *whtlst_conf = conf; - int err; - - if (!conf && size < sizeof(*whtlst_conf)) + if (!conf && size < sizeof(struct dp_vs_whtlst_conf)) return EDPVS_INVAL; switch (opt) { case SOCKOPT_SET_WHTLST_ADD: - err = dp_vs_whtlst_add(whtlst_conf->af, - whtlst_conf->proto, &whtlst_conf->vaddr, - whtlst_conf->vport, &whtlst_conf->whtlst); - break; + return dp_vs_whtlst_add(conf); case SOCKOPT_SET_WHTLST_DEL: - err = dp_vs_whtlst_del(whtlst_conf->af, - whtlst_conf->proto, &whtlst_conf->vaddr, - whtlst_conf->vport, &whtlst_conf->whtlst); - break; + return dp_vs_whtlst_del(conf); default: - err = EDPVS_NOTSUPP; - break; + return EDPVS_NOTSUPP; } - return err; -} - -static void whtlst_fill_conf(struct dp_vs_whtlst_conf *cf, - const struct whtlst_entry *entry) -{ - memset(cf, 0 ,sizeof(*cf)); - cf->af = entry->af; - cf->vaddr = entry->vaddr; - cf->whtlst = entry->whtlst; - cf->proto = entry->proto; - cf->vport = entry->vport; + return EDPVS_OK; } static int whtlst_sockopt_get(sockoptid_t opt, const void *conf, size_t size, @@ -309,7 +445,7 @@ static int whtlst_sockopt_get(sockoptid_t opt, const void *conf, size_t size, size_t naddr, hash; int off = 0; - naddr = rte_atomic32_read(&this_num_whtlsts); + naddr = this_num_whtlsts + this_num_whtlsts_ipset; *outsize = sizeof(struct dp_vs_whtlst_conf_array) + naddr * sizeof(struct dp_vs_whtlst_conf); *out = rte_calloc(NULL, 1, *outsize, 0); @@ -322,7 +458,15 @@ static int whtlst_sockopt_get(sockoptid_t opt, const void *conf, size_t size, list_for_each_entry(entry, &this_whtlst_tab[hash], list) { if (off >= naddr) break; - whtlst_fill_conf(&array->whtlsts[off++], entry); + whtlst_fill_conf(entry, &array->whtlsts[off++]); + } + } + + for (hash = 0; hash < DPVS_WHTLST_IPSET_TAB_SIZE; hash++) { + list_for_each_entry(entry, &this_whtlst_ipset_tab[hash], list) { + if (off >= naddr) + break; + whtlst_fill_conf(entry, &array->whtlsts[off++]); } } @@ -332,35 +476,35 @@ static int whtlst_sockopt_get(sockoptid_t opt, const void *conf, size_t size, static int whtlst_msg_process(bool add, struct dpvs_msg *msg) { - struct dp_vs_whtlst_conf *cf; + struct dp_vs_whtlst_conf *conf; int err; assert(msg); if (msg->len != sizeof(struct dp_vs_whtlst_conf)) { - RTE_LOG(ERR, SERVICE, "%s: bad message.\n", __func__); + RTE_LOG(ERR, SERVICE, "%s: bad message\n", __func__); return EDPVS_INVAL; } - cf = (struct dp_vs_whtlst_conf *)msg->data; + conf = (struct dp_vs_whtlst_conf *)msg->data; if (add) { - err = dp_vs_whtlst_add_lcore(cf->af, cf->proto, &cf->vaddr, cf->vport, &cf->whtlst); - if (err && err != EDPVS_EXIST) { - RTE_LOG(ERR, SERVICE, "%s: fail to add whtlst: %s.\n", __func__, dpvs_strerror(err)); - } - } - else { - err = dp_vs_whtlst_del_lcore(cf->af, cf->proto, &cf->vaddr, cf->vport, &cf->whtlst); + err = dp_vs_whtlst_add_lcore(conf); + if (err && err != EDPVS_EXIST) + RTE_LOG(ERR, SERVICE, "%s: fail to add whtlst: %s\n", __func__, dpvs_strerror(err)); + } else { + err = dp_vs_whtlst_del_lcore(conf); + if (err && err != EDPVS_NOTEXIST) + RTE_LOG(ERR, SERVICE, "%s: fail to del whtlst: %s\n", __func__, dpvs_strerror(err)); } return err; } -inline static int whtlst_add_msg_cb(struct dpvs_msg *msg) +static inline int whtlst_add_msg_cb(struct dpvs_msg *msg) { return whtlst_msg_process(true, msg); } -inline static int whtlst_del_msg_cb(struct dpvs_msg *msg) +static inline int whtlst_del_msg_cb(struct dpvs_msg *msg) { return whtlst_msg_process(false, msg); } @@ -378,17 +522,29 @@ static struct dpvs_sockopts whtlst_sockopts = { static int whtlst_lcore_init(void *args) { int i; + if (!rte_lcore_is_enabled(rte_lcore_id())) return EDPVS_DISABLED; - this_whtlst_tab = rte_malloc(NULL, - sizeof(struct list_head) * DPVS_WHTLST_TAB_SIZE, - RTE_CACHE_LINE_SIZE); + + this_num_whtlsts = 0; + this_num_whtlsts_ipset = 0; + + this_whtlst_tab = rte_malloc(NULL, sizeof(struct list_head) * + DPVS_WHTLST_TAB_SIZE, RTE_CACHE_LINE_SIZE); if (!this_whtlst_tab) return EDPVS_NOMEM; - for (i = 0; i < DPVS_WHTLST_TAB_SIZE; i++) INIT_LIST_HEAD(&this_whtlst_tab[i]); + this_whtlst_ipset_tab = rte_malloc(NULL, sizeof(struct list_head) * + DPVS_WHTLST_IPSET_TAB_SIZE, RTE_CACHE_LINE_SIZE); + if (!this_whtlst_ipset_tab) { + rte_free(this_whtlst_tab); + return EDPVS_NOMEM; + } + for (i = 0; i < DPVS_WHTLST_IPSET_TAB_SIZE; i++) + INIT_LIST_HEAD(&this_whtlst_ipset_tab[i]); + return EDPVS_OK; } @@ -403,13 +559,19 @@ static int whtlst_lcore_term(void *args) rte_free(this_whtlst_tab); this_whtlst_tab = NULL; } + + if (this_whtlst_ipset_tab) { + rte_free(this_whtlst_ipset_tab); + this_whtlst_ipset_tab = NULL; + } + return EDPVS_OK; } -static int whtlst_unregister_msg_cb(void) +static void whtlst_unregister_msg_cb(void) { - struct dpvs_msg_type msg_type; int err; + struct dpvs_msg_type msg_type; memset(&msg_type, 0, sizeof(struct dpvs_msg_type)); msg_type.type = MSG_TYPE_WHTLST_ADD; @@ -419,8 +581,8 @@ static int whtlst_unregister_msg_cb(void) msg_type.unicast_msg_cb = whtlst_add_msg_cb; err = msg_type_mc_unregister(&msg_type); if (err != EDPVS_OK) { - RTE_LOG(ERR, SERVICE, "%s: fail to unregister msg.\n", __func__); - return err; + RTE_LOG(WARNING, SERVICE, "%s: unregister WHTLST_ADD msg failed -- %s\n", + __func__, dpvs_strerror(err)); } memset(&msg_type, 0, sizeof(struct dpvs_msg_type)); @@ -431,10 +593,9 @@ static int whtlst_unregister_msg_cb(void) msg_type.unicast_msg_cb = whtlst_del_msg_cb; err = msg_type_mc_unregister(&msg_type); if (err != EDPVS_OK) { - RTE_LOG(ERR, SERVICE, "%s: fail to unregister msg.\n", __func__); - return err; + RTE_LOG(WARNING, SERVICE, "%s: unregister WHTLIST_DEL msg failed -- %s\n", + __func__, dpvs_strerror(err)); } - return EDPVS_OK; } int dp_vs_whtlst_init(void) @@ -443,13 +604,11 @@ int dp_vs_whtlst_init(void) lcoreid_t cid; struct dpvs_msg_type msg_type; - rte_atomic32_set(&this_num_whtlsts, 0); - rte_eal_mp_remote_launch(whtlst_lcore_init, NULL, CALL_MAIN); RTE_LCORE_FOREACH_WORKER(cid) { if ((err = rte_eal_wait_lcore(cid)) < 0) { - RTE_LOG(WARNING, SERVICE, "%s: lcore %d: %s.\n", - __func__, cid, dpvs_strerror(err)); + RTE_LOG(WARNING, SERVICE, "[%02d] %s: whtlst init failed -- %s\n", + cid, __func__, dpvs_strerror(err)); return err; } } @@ -462,7 +621,8 @@ int dp_vs_whtlst_init(void) msg_type.unicast_msg_cb = whtlst_add_msg_cb; err = msg_type_mc_register(&msg_type); if (err != EDPVS_OK) { - RTE_LOG(ERR, SERVICE, "%s: fail to register msg.\n", __func__); + RTE_LOG(ERR, SERVICE, "%s: register WHTLST_ADD msg failed -- %s\n", + __func__, dpvs_strerror(err)); return err; } @@ -474,14 +634,18 @@ int dp_vs_whtlst_init(void) msg_type.unicast_msg_cb = whtlst_del_msg_cb; err = msg_type_mc_register(&msg_type); if (err != EDPVS_OK) { - RTE_LOG(ERR, SERVICE, "%s: fail to register msg.\n", __func__); + RTE_LOG(ERR, SERVICE, "%s: register WHTLST_DEL msg failed -- %s\n", + __func__, dpvs_strerror(err)); return err; } if ((err = sockopt_register(&whtlst_sockopts)) != EDPVS_OK) { + RTE_LOG(ERR, SERVICE, "%s: register sockopts failed -- %s\n", + __func__, dpvs_strerror(err)); whtlst_unregister_msg_cb(); return err; } + dp_vs_whtlst_rnd = (uint32_t)random(); return EDPVS_OK; @@ -492,17 +656,18 @@ int dp_vs_whtlst_term(void) int err; lcoreid_t cid; - if ((err = whtlst_unregister_msg_cb()) != EDPVS_OK) - return err; + if ((err = sockopt_unregister(&whtlst_sockopts)) != EDPVS_OK) { + RTE_LOG(WARNING, SERVICE, "%s: unregister sockopts failed -- %s\n", + __func__, dpvs_strerror(err)); + } - if ((err = sockopt_unregister(&whtlst_sockopts)) != EDPVS_OK) - return err; + whtlst_unregister_msg_cb(); rte_eal_mp_remote_launch(whtlst_lcore_term, NULL, CALL_MAIN); RTE_LCORE_FOREACH_WORKER(cid) { if ((err = rte_eal_wait_lcore(cid)) < 0) { - RTE_LOG(WARNING, SERVICE, "%s: lcore %d: %s.\n", - __func__, cid, dpvs_strerror(err)); + RTE_LOG(WARNING, SERVICE, "[%02d] %s: whtlst termination failed -- %s\n", + cid, __func__, dpvs_strerror(err)); } } diff --git a/tools/ipvsadm/ipvsadm.c b/tools/ipvsadm/ipvsadm.c index 69c8e81c4..4938cedd0 100644 --- a/tools/ipvsadm/ipvsadm.c +++ b/tools/ipvsadm/ipvsadm.c @@ -920,15 +920,19 @@ parse_options(int argc, char **argv, struct ipvs_command_entry *ce, { dpvs_service_compat_t dpvs_svc; set_option(options,OPT_WHTLST_ADDRESS); - parse = parse_service(optarg, - &dpvs_svc); - if (!(parse & SERVICE_ADDR)) - fail(2, "illegal whitelist address"); - - ce->dpvs_whtlst.af = dpvs_svc.af; - ce->dpvs_whtlst.whtlst = dpvs_svc.addr; + if (!strncmp(optarg, "ipset:", strlen("ipset:"))) { + strncpy(ce->dpvs_whtlst.ipset, &optarg[strlen("ipset:")], + sizeof(ce->dpvs_whtlst.ipset) - 1); + } else { + parse = parse_service(optarg, &dpvs_svc); + if (parse & SERVICE_ADDR) { + ce->dpvs_whtlst.af = dpvs_svc.af; + ce->dpvs_whtlst.subject = dpvs_svc.addr; + } else { + fail(2, "illegal whitelist entry format, require [ IP | ipset:NAME ]"); + } + } break; - } case 'F': set_option(options, OPT_IFNAME); @@ -1689,9 +1693,9 @@ static void usage_exit(const char *program, const int exit_status) " --add-blklst -U add blacklist address or ipset\n" " --del-blklst -V del blacklist address or ipset\n" " --get-blklst -B get blacklist address or ipset\n" - " --add-whtlst -O add whitelist address\n" - " --del-whtlst -Y del whitelist address\n" - " --get-whtlst -W get whitelist address\n" + " --add-whtlst -O add whitelist address or ipset\n" + " --del-whtlst -Y del whitelist address or ipset\n" + " --get-whtlst -W get whitelist address or ipset\n" " --save -S save rules to stdout\n" " --add-server -a add real server with options\n" " --edit-server -e edit real server with options\n" @@ -1757,7 +1761,7 @@ static void usage_exit(const char *program, const int exit_status) " DOWNONLY:=down_retry,down_wait, for example, --dest-check=1,3s\n" " --laddr -z local-ip local IP\n" " --blklst -k blacklist-ip specify blacklist ip address or ipset(format: \"ipset:NAME\")\n" - " --whtlst -2 whitelist-ip whitelist IP for specific service\n" + " --whtlst -2 whitelist-ip specify whitelist ip address or ipset(format: \"ipset:NAME\")\n" " --quic itef quic protocol service\n", DEF_SCHED); @@ -2436,8 +2440,7 @@ static void list_blklsts_print_title(void) static void print_service_and_blklsts(const struct dp_vs_blklst_conf *blklst) { char subject[64], vip[64], vport[8], proto[8], vip_port[64]; - const char *pattern = (blklst->af == AF_INET ? - "%-8s %-30s %-30s\n" : "%-8s %-30s %-30s\n"); + const char *pattern = "%-8s %-30s %-30s\n"; switch (blklst->proto) { case IPPROTO_TCP: @@ -2487,7 +2490,8 @@ static bool inet_addr_equal(int af, const union inet_addr *a1, const union inet_ } static inline void __list_blklst(int af, const union inet_addr *addr, uint16_t port, - uint16_t protocol, const struct dp_vs_blklst_conf_array *cfarr) { + uint16_t protocol, const struct dp_vs_blklst_conf_array *cfarr) +{ int i; for (i = 0; i < cfarr->naddr; i++) { if (inet_addr_equal(af, addr, (const union inet_addr *) &cfarr->blklsts[i].vaddr) && @@ -2515,7 +2519,7 @@ static int list_blklst(int af, const union inet_addr *addr, uint16_t port, uint1 static int list_all_blklsts(void) { int i; - dpvs_services_front_t* table; + dpvs_services_front_t *table; struct dp_vs_blklst_conf_array *barray; table = (dpvs_services_front_t*)malloc(sizeof(dpvs_services_front_t)+sizeof(dpvs_service_compat_t)*g_ipvs_info.num_services); @@ -2551,17 +2555,16 @@ static int list_all_blklsts(void) static void list_whtlsts_print_title(void) { - printf("%-20s %-8s %-20s\n" , - "VIP:VPORT" , + printf("%-8s %-30s %-30s\n" , "PROTO" , + "VIP:VPORT" , "WHITELIST"); } -static void print_service_and_whtlsts(struct dp_vs_whtlst_conf *whtlst) +static void print_service_and_whtlsts(const struct dp_vs_whtlst_conf *whtlst) { - char vip[64], bip[64], port[8], proto[8]; - const char *pattern = (whtlst->af == AF_INET ? - "%s:%-8s %-8s %-20s\n" : "[%s]:%-8s %-8s %-20s\n"); + char subject[64], vip[64], vport[8], proto[8], vip_port[64]; + const char *pattern = "%-8s %-30s %-30s\n"; switch (whtlst->proto) { case IPPROTO_TCP: @@ -2583,15 +2586,35 @@ static void print_service_and_whtlsts(struct dp_vs_whtlst_conf *whtlst) break; } - snprintf(port, sizeof(port), "%u", ntohs(whtlst->vport)); + snprintf(vport, sizeof(vport), "%u", ntohs(whtlst->vport)); + inet_ntop(whtlst->af, (const void *)&whtlst->vaddr, vip, sizeof(vip)); + if (whtlst->af == AF_INET6) + snprintf(vip_port, sizeof(vip_port), "[%s]:%s", vip, vport); + else + snprintf(vip_port, sizeof(vip_port), "%s:%s", vip, vport); - printf(pattern, inet_ntop(whtlst->af, (const void *)&whtlst->vaddr, vip, sizeof(vip)), - port, proto, inet_ntop(whtlst->af, (const void *)&whtlst->whtlst, bip, sizeof(bip))); + if (whtlst->ipset[0] == '\0') + inet_ntop(whtlst->af, (const void *)&whtlst->subject, subject, sizeof(subject)); + else + snprintf(subject, sizeof(subject), "ipset:%s", whtlst->ipset); + + printf(pattern, proto, vip_port, subject); } -static int list_whtlst(int af, const union inet_addr *addr, uint16_t port, uint16_t protocol) +static inline void __list_whtlst(int af, const union inet_addr *addr, uint16_t port, + uint16_t protocol, const struct dp_vs_whtlst_conf_array *cfarr) { int i; + for (i = 0; i < cfarr->naddr; i++) { + if (inet_addr_equal(af, addr,(const union inet_addr *) &cfarr->whtlsts[i].vaddr) && + port == cfarr->whtlsts[i].vport && protocol == cfarr->whtlsts[i].proto) { + print_service_and_whtlsts(&cfarr->whtlsts[i]); + } + } +} + +static int list_whtlst(int af, const union inet_addr *addr, uint16_t port, uint16_t protocol) +{ struct dp_vs_whtlst_conf_array *get; if (!(get = dpvs_get_whtlsts())) { @@ -2599,22 +2622,17 @@ static int list_whtlst(int af, const union inet_addr *addr, uint16_t port, uint1 return -1; } - for (i = 0; i < get->naddr; i++) { - if (inet_addr_equal(af, addr,(const union inet_addr *) &get->whtlsts[i].vaddr) && - port == get->whtlsts[i].vport && protocol == get->whtlsts[i].proto) { - print_service_and_whtlsts(&get->whtlsts[i]); - } - } + __list_whtlst(af, addr, port, protocol, get); free(get); - return 0; } static int list_all_whtlsts(void) { - dpvs_services_front_t* table; int i; + dpvs_services_front_t *table; + struct dp_vs_whtlst_conf_array *warray; table = (dpvs_services_front_t*)malloc(sizeof(dpvs_services_front_t)+sizeof(dpvs_service_compat_t)*g_ipvs_info.num_services); if (!table) { @@ -2630,12 +2648,18 @@ static int list_all_whtlsts(void) exit(1); } + if (!(warray = dpvs_get_whtlsts())) { + fprintf(stderr, "%s\n", ipvs_strerror(errno)); + exit(1); + } + list_whtlsts_print_title(); for (i = 0; i < table->count; i++) { - list_whtlst(table->entrytable[i].af, &table->entrytable[i].addr, - table->entrytable[i].port, table->entrytable[i].proto); + __list_whtlst(table->entrytable[i].af, &table->entrytable[i].addr, + table->entrytable[i].port, table->entrytable[i].proto, warray); } + free(warray); free(table); return 0; diff --git a/tools/keepalived/keepalived/check/check_data.c b/tools/keepalived/keepalived/check/check_data.c index b696de6de..679a7653a 100644 --- a/tools/keepalived/keepalived/check/check_data.c +++ b/tools/keepalived/keepalived/check/check_data.c @@ -106,8 +106,10 @@ free_whtlst_group(void *data) FREE_PTR(whtlst_group->gname); free_list(&whtlst_group->addr_ip); free_list(&whtlst_group->range); + free_list(&whtlst_group->ipset); FREE(whtlst_group); } + static void dump_whtlst_group(FILE *fp, const void *data) { @@ -116,18 +118,23 @@ dump_whtlst_group(FILE *fp, const void *data) conf_write(fp, " whitelist IP address group = %s", whtlst_group->gname); dump_list(fp, whtlst_group->addr_ip); dump_list(fp, whtlst_group->range); + dump_list(fp, whtlst_group->ipset); } + static void free_whtlst_entry(void *data) { FREE(data); } + static void dump_whtlst_entry(FILE *fp, const void *data) { const whtlst_addr_entry *whtlst_entry = data; - if (whtlst_entry->range) + if (!strncmp(whtlst_entry->ipset, "ipset:", sizeof("ipset:") - 1)) + conf_write(fp, " IPSET = %s", whtlst_entry->ipset); + else if (whtlst_entry->range) conf_write(fp, " IP Range = %s-%d" , inet_sockaddrtos(&whtlst_entry->addr) , whtlst_entry->range); @@ -135,6 +142,7 @@ dump_whtlst_entry(FILE *fp, const void *data) conf_write(fp, " IP = %s" , inet_sockaddrtos(&whtlst_entry->addr)); } + void alloc_whtlst_group(char *gname) { @@ -146,21 +154,33 @@ alloc_whtlst_group(char *gname) memcpy(new->gname, gname, size); new->addr_ip = alloc_list(free_whtlst_entry, dump_whtlst_entry); new->range = alloc_list(free_whtlst_entry, dump_whtlst_entry); + new->ipset = alloc_list(free_whtlst_entry, dump_whtlst_entry); list_add(check_data->whtlst_group, new); } + void alloc_whtlst_entry(const vector_t *strvec) { whtlst_addr_group *whtlst_group = LIST_TAIL_DATA(check_data->whtlst_group); whtlst_addr_entry *new; + const char *str_entry; new = (whtlst_addr_entry *) MALLOC(sizeof (whtlst_addr_entry)); + if (!new) + return; + str_entry = strvec_slot(strvec, 0); + + if (!strncmp(str_entry, "ipset:", sizeof("ipset:") - 1)) { + strncpy(new->ipset, &str_entry[sizeof("ipset:")-1], sizeof(new->ipset) - 1); + list_add(whtlst_group->ipset, new); + return; + } - inet_stor(vector_slot(strvec, 0), &new->range); + inet_stor(str_entry, &new->range); if (new->range == UINT32_MAX) new->range = 0; - inet_stosockaddr(vector_slot(strvec, 0), NULL, &new->addr); + inet_stosockaddr(str_entry, NULL, &new->addr); if (!new->range) list_add(whtlst_group->addr_ip, new); @@ -257,7 +277,6 @@ alloc_vsg_entry(const vector_t *strvec) unsigned fwmark; new = (virtual_server_group_entry_t *) MALLOC(sizeof(virtual_server_group_entry_t)); - if (!strcmp(strvec_slot(strvec, 0), "fwmark")) { if (!read_unsigned_strvec(strvec, 1, &fwmark, 0, UINT32_MAX, true)) { report_config_error(CONFIG_GENERAL_ERROR, "(%s): fwmark '%s' must be in [0, %u] - ignoring", vsg->gname, strvec_slot(strvec, 1), UINT32_MAX); @@ -508,8 +527,11 @@ dump_vs(FILE *fp, const void *data) if (vs->blklst_addr_gname) conf_write(fp, " BLACK_LIST GROUP = %s", vs->blklst_addr_gname); + if (vs->whtlst_addr_gname) + conf_write(fp, " WHITE_LIST GROUP = %s", vs->whtlst_addr_gname); + if (vs->vip_bind_dev) - conf_write(fp, " vip_bind_dev = %s", vs->blklst_addr_gname); + conf_write(fp, " vip_bind_dev = %s", vs->vip_bind_dev); conf_write(fp, " SYN proxy is %s", vs->syn_proxy ? "ON" : "OFF"); conf_write(fp, " expire_quiescent_conn is %s", vs->expire_quiescent_conn ? "ON" : "OFF"); @@ -560,8 +582,7 @@ alloc_vs(const char *param1, const char *param2) new->vfwmark = fwmark; } else if (!strcmp(param1, "match")) { - new->forwarding_method = IP_VS_CONN_F_SNAT; - + new->forwarding_method = IP_VS_CONN_F_SNAT; } else { /* Don't pass a zero for port number to inet_stosockaddr. This was added in v2.0.7 * to support legacy configuration since previously having no port wasn't allowed. */ @@ -622,8 +643,8 @@ alloc_vs(const char *param1, const char *param2) } /*local address group facility functions*/ -static void -free_laddr_group(void *data) +static void +free_laddr_group(void *data) { local_addr_group *laddr_group = (local_addr_group*)data; FREE_PTR(laddr_group->gname); @@ -708,6 +729,7 @@ free_blklst_group(void *data) FREE_PTR(blklst_group->gname); free_list(&blklst_group->addr_ip); free_list(&blklst_group->range); + free_list(&blklst_group->ipset); FREE(blklst_group); } @@ -719,6 +741,7 @@ dump_blklst_group(FILE *fp, const void *data) conf_write(fp, " blacllist IP address group = %s", blklst_group->gname); dump_list(fp, blklst_group->addr_ip); dump_list(fp, blklst_group->range); + dump_list(fp, blklst_group->ipset); } static void @@ -767,6 +790,8 @@ alloc_blklst_entry(const vector_t *strvec) const char *str_entry; new = (blklst_addr_entry *) MALLOC(sizeof (blklst_addr_entry)); + if (!new) + return; str_entry = strvec_slot(strvec, 0); if (!strncmp(str_entry, "ipset:", sizeof("ipset:") - 1)) { @@ -1063,7 +1088,7 @@ alloc_check_data(void) #endif new->laddr_group = alloc_list(free_laddr_group, dump_laddr_group); new->blklst_group = alloc_list(free_blklst_group, dump_blklst_group); - new->whtlst_group = alloc_list(free_whtlst_group, dump_whtlst_group); + new->whtlst_group = alloc_list(free_whtlst_group, dump_whtlst_group); new->tunnel_group = alloc_list(free_tunnel_group, dump_tunnel_group); return new; @@ -1081,6 +1106,7 @@ free_check_data(check_data_t *data) #endif free_list(&data->laddr_group); free_list(&data->blklst_group); + free_list(&data->whtlst_group); free_list(&data->tunnel_group); FREE(data); } @@ -1100,6 +1126,8 @@ dump_check_data(FILE *fp, check_data_t *data) dump_list(fp, data->laddr_group); if (!LIST_ISEMPTY(data->blklst_group)) dump_list(fp, data->blklst_group); + if (!LIST_ISEMPTY(data->whtlst_group)) + dump_list(fp, data->whtlst_group); if (!LIST_ISEMPTY(data->vs_group)) dump_list(fp, data->vs_group); dump_list(fp, data->vs); diff --git a/tools/keepalived/keepalived/check/ipvswrapper.c b/tools/keepalived/keepalived/check/ipvswrapper.c index 5183f11d6..9a1398da4 100755 --- a/tools/keepalived/keepalived/check/ipvswrapper.c +++ b/tools/keepalived/keepalived/check/ipvswrapper.c @@ -865,8 +865,8 @@ ipvs_whtlst_range_cmd(int cmd, whtlst_addr_entry *whtlst_entry, dpvs_service_com memset(&whtlst_rule, 0, sizeof(dpvs_whtlst_t)); whtlst_rule.af = whtlst_entry->addr.ss_family; if (whtlst_entry->addr.ss_family == AF_INET6) { - inet_sockaddrip6(&whtlst_entry->addr, &whtlst_rule.whtlst.in6); - ip = whtlst_rule.whtlst.in6.s6_addr32[3]; + inet_sockaddrip6(&whtlst_entry->addr, &whtlst_rule.subject.in6); + ip = whtlst_rule.subject.in6.s6_addr32[3]; } else { ip = inet_sockaddrip4(&whtlst_entry->addr); } @@ -874,9 +874,9 @@ ipvs_whtlst_range_cmd(int cmd, whtlst_addr_entry *whtlst_entry, dpvs_service_com for (addr_ip = ip; ((addr_ip >> 24) & 0xFF) <= whtlst_entry->range; addr_ip += 0x01000000) { if (whtlst_entry->addr.ss_family == AF_INET6) - whtlst_rule.whtlst.in6.s6_addr32[3] = addr_ip; + whtlst_rule.subject.in6.s6_addr32[3] = addr_ip; else - whtlst_rule.whtlst.in.s_addr = addr_ip; + whtlst_rule.subject.in.s_addr = addr_ip; ipvs_talk(cmd, srule, NULL, NULL, NULL, NULL, &whtlst_rule, NULL, false); } @@ -898,9 +898,9 @@ ipvs_whtlst_group_cmd(int cmd, whtlst_addr_group *whtlst_group, dpvs_service_com memset(&whtlst_rule, 0, sizeof(dpvs_whtlst_t)); whtlst_rule.af = whtlst_entry->addr.ss_family; if (whtlst_entry->addr.ss_family == AF_INET6) - inet_sockaddrip6(&whtlst_entry->addr, &whtlst_rule.whtlst.in6); + inet_sockaddrip6(&whtlst_entry->addr, &whtlst_rule.subject.in6); else - whtlst_rule.whtlst.in.s_addr = inet_sockaddrip4(&whtlst_entry->addr); + whtlst_rule.subject.in.s_addr = inet_sockaddrip4(&whtlst_entry->addr); ipvs_talk(cmd, srule, NULL, NULL, NULL, NULL, &whtlst_rule, NULL, false); } @@ -908,6 +908,13 @@ ipvs_whtlst_group_cmd(int cmd, whtlst_addr_group *whtlst_group, dpvs_service_com LIST_FOREACH(l, whtlst_entry, e) { ipvs_whtlst_range_cmd(cmd, whtlst_entry, srule); } + + l = whtlst_group->ipset; + LIST_FOREACH(l, whtlst_entry, e) { + memset(&whtlst_rule, 0, sizeof(dpvs_whtlst_t)); + strncpy(whtlst_rule.ipset, whtlst_entry->ipset, sizeof(whtlst_rule.ipset) - 1); + ipvs_talk(cmd, srule, NULL, NULL, NULL, NULL, &whtlst_rule, NULL, false); + } } static void @@ -1305,16 +1312,19 @@ ipvs_rm_bentry_from_vsg(blklst_addr_entry *blklst_entry, whtlst_addr_entry *whtl } } if (whtlst_entry != NULL) { - if (whtlst_entry->range) + if (whtlst_entry->ipset[0] != '\0') { + memset(&whtlst_rule, 0, sizeof(dpvs_whtlst_t)); + strncpy(whtlst_rule.ipset, whtlst_entry->ipset, sizeof(whtlst_rule.ipset) - 1); + ipvs_talk(IP_VS_SO_SET_DELWHTLST, srule, NULL, NULL, NULL, NULL, &whtlst_rule, NULL, false); + } else if (whtlst_entry->range) { ipvs_whtlst_range_cmd(IP_VS_SO_SET_DELWHTLST, whtlst_entry, srule); - else { + } else { memset(&whtlst_rule, 0, sizeof(dpvs_whtlst_t)); whtlst_rule.af = whtlst_entry->addr.ss_family; if (whtlst_entry->addr.ss_family == AF_INET6) - inet_sockaddrip6(&whtlst_entry->addr, &whtlst_rule.whtlst.in6); + inet_sockaddrip6(&whtlst_entry->addr, &whtlst_rule.subject.in6); else - whtlst_rule.whtlst.in.s_addr = inet_sockaddrip4(&whtlst_entry->addr); - + whtlst_rule.subject.in.s_addr = inet_sockaddrip4(&whtlst_entry->addr); ipvs_talk(IP_VS_SO_SET_DELWHTLST, srule, NULL, NULL, NULL, NULL, &whtlst_rule, NULL, false); } } @@ -1347,18 +1357,20 @@ ipvs_rm_bentry_from_vsg(blklst_addr_entry *blklst_entry, whtlst_addr_entry *whtl ipvs_talk(IP_VS_SO_SET_DELBLKLST, srule, NULL, NULL, NULL, &blklst_rule, NULL, NULL, false); } } - if (whtlst_entry != NULL) - { - if (whtlst_entry->range) + if (whtlst_entry != NULL) { + if (whtlst_entry->ipset[0] != '\0') { + memset(&whtlst_rule, 0, sizeof(dpvs_whtlst_t)); + strncpy(whtlst_rule.ipset, whtlst_entry->ipset, sizeof(whtlst_rule.ipset) - 1); + ipvs_talk(IP_VS_SO_SET_DELWHTLST, srule, NULL, NULL, NULL, NULL, &whtlst_rule, NULL, false); + } else if (whtlst_entry->range) { ipvs_whtlst_range_cmd(IP_VS_SO_SET_DELWHTLST, whtlst_entry, srule); - else { + } else { memset(&whtlst_rule, 0, sizeof(dpvs_whtlst_t)); whtlst_rule.af = whtlst_entry->addr.ss_family; if (whtlst_entry->addr.ss_family == AF_INET6) - inet_sockaddrip6(&whtlst_entry->addr, &whtlst_rule.whtlst.in6); + inet_sockaddrip6(&whtlst_entry->addr, &whtlst_rule.subject.in6); else - whtlst_rule.whtlst.in.s_addr = inet_sockaddrip4(&whtlst_entry->addr); - + whtlst_rule.subject.in.s_addr = inet_sockaddrip4(&whtlst_entry->addr); ipvs_talk(IP_VS_SO_SET_DELWHTLST, srule, NULL, NULL, NULL, NULL, &whtlst_rule, NULL, false); } } @@ -1431,15 +1443,19 @@ ipvs_whtlst_remove_entry(virtual_server_t *vs, whtlst_addr_entry *whtlst_entry) } srule.port = inet_sockaddrport(&vs->addr); - if (whtlst_entry->range) { + if (whtlst_entry->ipset[0] != '\0') { + memset(&whtlst_rule, 0, sizeof(dpvs_whtlst_t)); + strncpy(whtlst_rule.ipset, whtlst_entry->ipset, sizeof(whtlst_rule.ipset) - 1); + ipvs_talk(IP_VS_SO_SET_DELWHTLST, &srule, NULL, NULL, NULL, NULL, &whtlst_rule, NULL, false); + } else if (whtlst_entry->range) { ipvs_whtlst_range_cmd(IP_VS_SO_SET_DELWHTLST, whtlst_entry, &srule); } else { memset(&whtlst_rule, 0, sizeof(dpvs_whtlst_t)); whtlst_rule.af = whtlst_entry->addr.ss_family; if (whtlst_entry->addr.ss_family == AF_INET6) - inet_sockaddrip6(&whtlst_entry->addr, &whtlst_rule.whtlst.in6); + inet_sockaddrip6(&whtlst_entry->addr, &whtlst_rule.subject.in6); else - whtlst_rule.whtlst.in.s_addr = inet_sockaddrip4(&whtlst_entry->addr); + whtlst_rule.subject.in.s_addr = inet_sockaddrip4(&whtlst_entry->addr); ipvs_talk(IP_VS_SO_SET_DELWHTLST, &srule, NULL, NULL, NULL, NULL, &whtlst_rule, NULL, false); } diff --git a/tools/keepalived/keepalived/check/ipwrapper.c b/tools/keepalived/keepalived/check/ipwrapper.c index 1f8706a51..8b3fc470d 100755 --- a/tools/keepalived/keepalived/check/ipwrapper.c +++ b/tools/keepalived/keepalived/check/ipwrapper.c @@ -1171,7 +1171,8 @@ blklst_entry_exist(blklst_addr_entry *blklst_entry, list l) LIST_FOREACH(l, entry, e) { if (sockstorage_equal(&entry->addr, &blklst_entry->addr) && - entry->range == blklst_entry->range) + entry->range == blklst_entry->range && + !strncmp(entry->ipset, blklst_entry->ipset, sizeof(entry->ipset))) return 1; } return 0; @@ -1234,20 +1235,20 @@ clear_diff_blklst(virtual_server_t * old_vs, virtual_server_t * new_vs) if (!old) return 1; + if (new_vs->blklst_addr_gname) + new = ipvs_get_blklst_group_by_name(new_vs->blklst_addr_gname, + check_data->blklst_group); + /* if new_vs has no blacklist group, delete all blklst address from old_vs */ - if (!new_vs->blklst_addr_gname) { + if (!new) { if (!clear_all_blklst_entry(old->addr_ip, old_vs)) return 0; if (!clear_all_blklst_entry(old->range, old_vs)) return 0; + if (!clear_all_blklst_entry(old->ipset, old_vs)) + return 0; return 1; } - else - /* Fetch new_vs blacklist address group */ - new = ipvs_get_blklst_group_by_name(new_vs->blklst_addr_gname, - check_data->blklst_group); - if (!new) - return 1; if (!clear_diff_blklst_entry(old->addr_ip, new->addr_ip, old_vs)) return 0; @@ -1269,8 +1270,9 @@ whtlst_entry_exist(whtlst_addr_entry *whtlst_entry, list l) for (e = LIST_HEAD(l); e; ELEMENT_NEXT(e)) { entry = ELEMENT_DATA(e); if (sockstorage_equal(&entry->addr, &whtlst_entry->addr) && - entry->range == whtlst_entry->range) - return 1; + entry->range == whtlst_entry->range && + !strncmp(entry->ipset, whtlst_entry->ipset, sizeof(entry->ipset))) + return 1; } return 0; } @@ -1329,28 +1331,30 @@ clear_diff_whtlst(virtual_server_t * old_vs, virtual_server_t * new_vs) /* Fetch whitelist address group */ old = ipvs_get_whtlst_group_by_name(old_vs->whtlst_addr_gname, old_check_data->whtlst_group); - if (!old) return 1; + + if (new_vs->whtlst_addr_gname) + new = ipvs_get_whtlst_group_by_name(new_vs->whtlst_addr_gname, + check_data->whtlst_group); + /* if new_vs has no whitelist group, delete all whtlst address from old_vs */ - if (!new_vs->whtlst_addr_gname) { + if (!new) { if (!clear_all_whtlst_entry(old->addr_ip, old_vs)) return 0; if (!clear_all_whtlst_entry(old->range, old_vs)) return 0; + if (!clear_all_whtlst_entry(old->ipset, old_vs)) + return 0; return 1; } - else - /* Fetch new_vs whitelist address group */ - new = ipvs_get_whtlst_group_by_name(new_vs->whtlst_addr_gname, - check_data->whtlst_group); - if (!new) - return 1; if (!clear_diff_whtlst_entry(old->addr_ip, new->addr_ip, old_vs)) return 0; if (!clear_diff_whtlst_entry(old->range, new->range, old_vs)) return 0; + if (!clear_diff_whtlst_entry(old->ipset, new->ipset, old_vs)) + return 0; return 1; } diff --git a/tools/keepalived/keepalived/check/libipvs.c b/tools/keepalived/keepalived/check/libipvs.c index 8d9096795..08e41ed0f 100644 --- a/tools/keepalived/keepalived/check/libipvs.c +++ b/tools/keepalived/keepalived/check/libipvs.c @@ -329,14 +329,7 @@ static void dpvs_fill_whtlst_conf(dpvs_service_compat_t *svc, dpvs_whtlst_t *wht whtlst->af = svc->af; whtlst->proto = svc->proto; whtlst->vport = svc->port; - whtlst->fwmark = svc->fwmark; - if (svc->af == AF_INET) { - whtlst->vaddr.in = svc->addr.in; - } else { - whtlst->vaddr.in6 = svc->addr.in6; - } - - return; + whtlst->vaddr = svc->addr; } int dpvs_add_whtlst(dpvs_service_compat_t* svc, dpvs_whtlst_t *whtlst) diff --git a/tools/keepalived/keepalived/include/check_data.h b/tools/keepalived/keepalived/include/check_data.h index 28aec2258..6b32141e6 100644 --- a/tools/keepalived/keepalived/include/check_data.h +++ b/tools/keepalived/keepalived/include/check_data.h @@ -147,12 +147,14 @@ typedef struct _blklst_addr_group { typedef struct _whtlst_addr_entry { struct sockaddr_storage addr; uint32_t range; + char ipset[IPSET_MAXNAMELEN]; } whtlst_addr_entry; typedef struct _whtlst_addr_group { char *gname; list addr_ip; list range; + list ipset; } whtlst_addr_group; typedef struct _tunnel_entry { From 19f401fdb20fad7519c863b65cd44abf1a559be1 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Tue, 18 Jun 2024 20:18:18 +0800 Subject: [PATCH 45/63] tools/dpip: fix ipset list-all problem and improve efficiency Signed-off-by: ywc689 --- tools/dpip/ipset.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/dpip/ipset.c b/tools/dpip/ipset.c index baeee9257..c6e954299 100644 --- a/tools/dpip/ipset.c +++ b/tools/dpip/ipset.c @@ -1081,7 +1081,11 @@ ipset_info_dump(struct ipset_info *info, bool sort) struct ipset_member *member; char header[HEADER_LEN], *members; - type = get_type_idx(); + type = get_type_idx_from_type(info->type); + if (type < 0) { + fprintf(stderr, "unsupported ipset type %s\n", info->type); + return; + } /* header */ types[type].dump_header(header, info); From 5d7a7a51b23de10cf91e4ca754fda543e5c5140c Mon Sep 17 00:00:00 2001 From: ywc689 Date: Wed, 19 Jun 2024 16:44:43 +0800 Subject: [PATCH 46/63] tools/dpvs-agent: adapted for ipset-type allow/deny list Signed-off-by: ywc689 --- .../cmd/dpvs-agent-server/api_init.go | 2 +- .../cmd/ipvs/delete_vs_vip_port_allow.go | 22 ++++-- .../cmd/ipvs/delete_vs_vip_port_deny.go | 22 ++++-- .../cmd/ipvs/put_vs_vip_port_allow.go | 22 ++++-- .../cmd/ipvs/put_vs_vip_port_deny.go | 22 ++++-- tools/dpvs-agent/dpvs-agent-api.yaml | 2 + tools/dpvs-agent/models/cert_auth_spec.go | 3 + tools/dpvs-agent/pkg/ipc/types/certificate.go | 79 ++++++++++++------- tools/dpvs-agent/restapi/embedded_spec.go | 6 ++ 9 files changed, 129 insertions(+), 51 deletions(-) diff --git a/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go b/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go index 7912acbce..80b00831e 100644 --- a/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go +++ b/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go @@ -25,7 +25,7 @@ import ( "time" "github.com/hashicorp/go-hclog" - "github.com/lestrrat-go/file-rotatelogs" + rotatelogs "github.com/lestrrat-go/file-rotatelogs" "github.com/dpvs-agent/cmd/device" "github.com/dpvs-agent/cmd/ipvs" diff --git a/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port_allow.go b/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port_allow.go index f1e068e5d..ab04f776e 100644 --- a/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port_allow.go +++ b/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port_allow.go @@ -16,6 +16,7 @@ package ipvs import ( "net" + "strings" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" @@ -48,18 +49,29 @@ func (h *delVsAllow) Handle(params apiVs.DeleteVsVipPortAllowParams) middleware. failed := false for _, allow := range params.ACL.Items { - if net.ParseIP(allow.Addr) == nil { - h.logger.Error("Invalid ip addr del.", "VipPort", params.VipPort, "Addr", allow.Addr) - return apiVs.NewDeleteVsVipPortAllowInvalidFrontend() + spec.SetCaddr("") + spec.SetIpset("") + if len(allow.Ipset) > 0 { + if !strings.HasPrefix(allow.Ipset, "ipset:") { + h.logger.Error("Invalid allow ipset format in del.", "VipPort", params.VipPort, + "Ipset", allow.Ipset, "expecting \"ipset:NAME\"") + return apiVs.NewPutVsVipPortAllowInvalidFrontend() + } + spec.SetIpset(allow.Ipset) + } else { + if net.ParseIP(allow.Addr) == nil { + h.logger.Error("Invalid ip addr del in del.", "VipPort", params.VipPort, "Addr", allow.Addr) + return apiVs.NewDeleteVsVipPortAllowInvalidFrontend() + } + spec.SetCaddr(allow.Addr) } - spec.SetSrc(allow.Addr) if result := spec.Del(h.connPool, false, h.logger); result != types.EDPVS_OK { failed = true h.logger.Error("IP Addr delete from white list failed.", "VipPort", params.VipPort, "Addr", allow.Addr, "result", result.String()) continue } - h.logger.Info("IP Addr delete from white list success.", "VipPort", params.VipPort, "Addr", allow.Addr) + h.logger.Info("Delete entry from black list success.", "VipPort", params.VipPort, "Addr", allow.Addr, "Ipset", allow.Ipset) } if failed { diff --git a/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port_deny.go b/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port_deny.go index 54d2bb9d2..f328056b2 100644 --- a/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port_deny.go +++ b/tools/dpvs-agent/cmd/ipvs/delete_vs_vip_port_deny.go @@ -16,6 +16,7 @@ package ipvs import ( "net" + "strings" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/ipc/types" @@ -48,18 +49,29 @@ func (h *delVsDeny) Handle(params apiVs.DeleteVsVipPortDenyParams) middleware.Re failed := false for _, deny := range params.ACL.Items { - if net.ParseIP(deny.Addr) == nil { - h.logger.Error("Invalid ip addr del.", "VipPort", params.VipPort, "Addr", deny.Addr) - return apiVs.NewDeleteVsVipPortDenyInvalidFrontend() + spec.SetCaddr("") + spec.SetIpset("") + if len(deny.Ipset) > 0 { + if !strings.HasPrefix(deny.Ipset, "ipset:") { + h.logger.Error("Invalid deny ipset format in del.", "VipPort", params.VipPort, + "Ipset", deny.Ipset, "expecting \"ipset:NAME\"") + return apiVs.NewPutVsVipPortDenyInvalidFrontend() + } + spec.SetIpset(deny.Ipset) + } else { + if net.ParseIP(deny.Addr) == nil { + h.logger.Error("Invalid ip addr in del.", "VipPort", params.VipPort, "Addr", deny.Addr) + return apiVs.NewDeleteVsVipPortDenyInvalidFrontend() + } + spec.SetCaddr(deny.Addr) } - spec.SetSrc(deny.Addr) if result := spec.Del(h.connPool, true, h.logger); result != types.EDPVS_OK { h.logger.Error("IP Addr delete from black list failed.", "VipPort", params.VipPort, "Addr", deny.Addr, "result", result.String()) failed = true continue } - h.logger.Info("IP Addr delete from black list success.", "VipPort", params.VipPort, "Addr", deny.Addr) + h.logger.Info("Delete entry from black list success.", "VipPort", params.VipPort, "Addr", deny.Addr, "Ipset", deny.Ipset) } if failed { diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_allow.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_allow.go index a5ff5753a..5227b314b 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_allow.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_allow.go @@ -17,6 +17,7 @@ package ipvs import ( // "fmt" "net" + "strings" // "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" @@ -50,18 +51,29 @@ func (h *putVsAllow) Handle(params apiVs.PutVsVipPortAllowParams) middleware.Res failed := false for _, allow := range params.ACL.Items { - if net.ParseIP(allow.Addr) == nil { - h.logger.Error("Invalid ip addr add.", "VipPort", params.VipPort, "Addr", allow.Addr) - return apiVs.NewPutVsVipPortAllowInvalidFrontend() + spec.SetCaddr("") + spec.SetIpset("") + if len(allow.Ipset) > 0 { + if !strings.HasPrefix(allow.Ipset, "ipset:") { + h.logger.Error("Invalid allow ipset format in add.", "VipPort", params.VipPort, + "Ipset", allow.Ipset, "expecting \"ipset:NAME\"") + return apiVs.NewPutVsVipPortAllowInvalidFrontend() + } + spec.SetIpset(allow.Ipset) + } else { + if net.ParseIP(allow.Addr) == nil { + h.logger.Error("Invalid ip addr add.", "VipPort", params.VipPort, "Addr", allow.Addr) + return apiVs.NewPutVsVipPortAllowInvalidFrontend() + } + spec.SetCaddr(allow.Addr) } - spec.SetSrc(allow.Addr) if result := spec.Add(h.connPool, false, h.logger); result != types.EDPVS_OK { failed = true h.logger.Error("Add ip addr to white list failed.", "VipPort", params.VipPort, "Addr", allow.Addr, "result", result.String()) continue } - h.logger.Info("Add ip addr to white list success.", "VipPort", params.VipPort, "Addr", allow.Addr) + h.logger.Info("Add entry to white list success.", "VipPort", params.VipPort, "Addr", allow.Addr, "Ipset", allow.Ipset) } if failed { diff --git a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_deny.go b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_deny.go index 632b563f6..5ff636871 100644 --- a/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_deny.go +++ b/tools/dpvs-agent/cmd/ipvs/put_vs_vip_port_deny.go @@ -17,6 +17,7 @@ package ipvs import ( // "fmt" "net" + "strings" // "github.com/dpvs-agent/models" "github.com/dpvs-agent/pkg/ipc/pool" @@ -50,18 +51,29 @@ func (h *putVsDeny) Handle(params apiVs.PutVsVipPortDenyParams) middleware.Respo failed := false for _, deny := range params.ACL.Items { - if net.ParseIP(deny.Addr) == nil { - h.logger.Error("Invalid ip addr add.", "VipPort", params.VipPort, "Addr", deny.Addr) - return apiVs.NewPutVsVipPortDenyInvalidFrontend() + spec.SetCaddr("") + spec.SetIpset("") + if len(deny.Ipset) > 0 { + if !strings.HasPrefix(deny.Ipset, "ipset:") { + h.logger.Error("Invalid deny ipset format in add.", "VipPort", params.VipPort, + "Ipset", deny.Ipset, "expecting \"ipset:NAME\"") + return apiVs.NewPutVsVipPortDenyInvalidFrontend() + } + spec.SetIpset(deny.Ipset) + } else { + if net.ParseIP(deny.Addr) == nil { + h.logger.Error("Invalid deny ip addr in add.", "VipPort", params.VipPort, "Addr", deny.Addr) + return apiVs.NewPutVsVipPortDenyInvalidFrontend() + } + spec.SetCaddr(deny.Addr) } - spec.SetSrc(deny.Addr) if result := spec.Add(h.connPool, true, h.logger); result != types.EDPVS_OK { h.logger.Error("Add ip addr to black list failed.", "VipPort", params.VipPort, "Addr", deny.Addr, "result", result.String()) failed = true continue } - h.logger.Info("Add ip addr to black list success.", "VipPort", params.VipPort, "Addr", deny.Addr) + h.logger.Info("Add entry to black list success.", "VipPort", params.VipPort, "Addr", deny.Addr, "Ipset", deny.Ipset) } if failed { diff --git a/tools/dpvs-agent/dpvs-agent-api.yaml b/tools/dpvs-agent/dpvs-agent-api.yaml index 8840cdafd..d466955e2 100644 --- a/tools/dpvs-agent/dpvs-agent-api.yaml +++ b/tools/dpvs-agent/dpvs-agent-api.yaml @@ -185,6 +185,8 @@ definitions: properties: addr: type: string + ipset: + type: string InetAddrSpec: properties: addr: diff --git a/tools/dpvs-agent/models/cert_auth_spec.go b/tools/dpvs-agent/models/cert_auth_spec.go index 61648012e..a7896bd28 100644 --- a/tools/dpvs-agent/models/cert_auth_spec.go +++ b/tools/dpvs-agent/models/cert_auth_spec.go @@ -19,6 +19,9 @@ type CertAuthSpec struct { // addr Addr string `json:"addr,omitempty"` + + // ipset + Ipset string `json:"ipset,omitempty"` } // Validate validates this cert auth spec diff --git a/tools/dpvs-agent/pkg/ipc/types/certificate.go b/tools/dpvs-agent/pkg/ipc/types/certificate.go index 5f8ea82a5..770a9bdeb 100644 --- a/tools/dpvs-agent/pkg/ipc/types/certificate.go +++ b/tools/dpvs-agent/pkg/ipc/types/certificate.go @@ -31,14 +31,22 @@ import ( "github.com/dpvs-agent/pkg/ipc/pool" ) +/* derived from: include/conf/ipset.h */ +const IPSET_MAXNAMELEN = 32 + +/* +derived from: + - include/conf/blklst.h + - include/conf/whtlst.h +*/ type CertificateAuthoritySpec struct { - src [0x10]byte - dst [0x10]byte - af uint32 - fwmark uint32 - port uint16 - proto uint8 - padding uint8 + vaddr [0x10]byte + vport uint16 + proto uint8 + af uint8 + + caddr [0x10]byte + ipset [IPSET_MAXNAMELEN]byte } type CertificateAuthorityFront struct { @@ -54,12 +62,12 @@ func NewCertificateAuthorityFront() *CertificateAuthorityFront { } func (o *CertificateAuthoritySpec) Copy(src *CertificateAuthoritySpec) bool { - o.af = src.af - o.fwmark = src.fwmark - o.port = src.port + copy(o.vaddr[:], src.vaddr[:]) + o.vport = src.vport o.proto = src.proto - copy(o.src[:], src.src[:]) - copy(o.dst[:], src.dst[:]) + o.af = src.af + copy(o.caddr[:], src.caddr[:]) + copy(o.ipset[:], src.ipset[:]) return true } @@ -80,19 +88,19 @@ func (o *CertificateAuthoritySpec) ParseVipPortProto(vipport string) error { o.proto = unix.IPPROTO_TCP } - // port := items[1] - port, err := strconv.Atoi(items[1]) + // vport := items[1] + vport, err := strconv.Atoi(items[1]) if err != nil { return err } - o.SetPort(uint16(port)) + o.SetVport(uint16(vport)) - vip := items[0] - if net.ParseIP(vip) == nil { - return errors.New(fmt.Sprintf("invalid ip addr: %s\n", vip)) + vaddr := items[0] + if net.ParseIP(vaddr) == nil { + return errors.New(fmt.Sprintf("invalid ip addr: %s\n", vaddr)) } - o.SetDst(vip) + o.SetVaddr(vaddr) return nil } @@ -150,42 +158,53 @@ func (o *CertificateAuthorityFront) GetCount() uint32 { return o.count } -func (o *CertificateAuthoritySpec) SetAf(af uint32) { +func (o *CertificateAuthoritySpec) SetAf(af uint8) { o.af = af } -func (o *CertificateAuthoritySpec) SetSrc(addr string) { +func (o *CertificateAuthoritySpec) SetCaddr(addr string) { + if len(addr) == 0 { + var zeros [0x10]byte + copy(o.caddr[:], zeros[:]) + return + } if strings.Contains(addr, ":") { o.SetAf(unix.AF_INET6) - copy(o.src[:], net.ParseIP(addr)) + copy(o.caddr[:], net.ParseIP(addr)) return } o.SetAf(unix.AF_INET) buf := new(bytes.Buffer) binary.Write(buf, binary.LittleEndian, net.ParseIP(addr)) - copy(o.src[:], buf.Bytes()[12:]) + copy(o.caddr[:], buf.Bytes()[12:]) } -func (o *CertificateAuthoritySpec) SetDst(addr string) { +func (o *CertificateAuthoritySpec) SetVaddr(addr string) { if strings.Contains(addr, ":") { o.SetAf(unix.AF_INET6) - copy(o.dst[:], net.ParseIP(addr)) + copy(o.vaddr[:], net.ParseIP(addr)) return } o.SetAf(unix.AF_INET) buf := new(bytes.Buffer) binary.Write(buf, binary.LittleEndian, net.ParseIP(addr)) - copy(o.dst[:], buf.Bytes()[12:]) + copy(o.vaddr[:], buf.Bytes()[12:]) } -func (o *CertificateAuthoritySpec) SetFwmark(fwmark uint32) { - o.fwmark = fwmark +func (o *CertificateAuthoritySpec) SetIpset(ipset string) { + if len(ipset) == 0 { + var zeros [IPSET_MAXNAMELEN]byte + copy(o.ipset[:], zeros[:]) + return + } + buf := []byte(ipset) + copy(o.ipset[:], buf[6:]) } -func (o *CertificateAuthoritySpec) SetPort(port uint16) { +func (o *CertificateAuthoritySpec) SetVport(port uint16) { buf := new(bytes.Buffer) binary.Write(buf, binary.LittleEndian, uint16(port)) - o.port = binary.BigEndian.Uint16(buf.Bytes()) + o.vport = binary.BigEndian.Uint16(buf.Bytes()) } func (o *CertificateAuthoritySpec) SetProto(proto string) { diff --git a/tools/dpvs-agent/restapi/embedded_spec.go b/tools/dpvs-agent/restapi/embedded_spec.go index e16e252cf..1e474f23c 100644 --- a/tools/dpvs-agent/restapi/embedded_spec.go +++ b/tools/dpvs-agent/restapi/embedded_spec.go @@ -1444,6 +1444,9 @@ func init() { "properties": { "addr": { "type": "string" + }, + "ipset": { + "type": "string" } } }, @@ -3997,6 +4000,9 @@ func init() { "properties": { "addr": { "type": "string" + }, + "ipset": { + "type": "string" } } }, From 30f601b9bd00144ee0b212bd38960baf0946e751 Mon Sep 17 00:00:00 2001 From: Peng Yong Date: Sun, 25 Aug 2024 22:26:09 +0800 Subject: [PATCH 47/63] define buffer size by INET6_ADDRSTRLEN --- src/ipvs/ip_vs_proxy_proto.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipvs/ip_vs_proxy_proto.c b/src/ipvs/ip_vs_proxy_proto.c index 8e2d35873..9daf7f2c8 100644 --- a/src/ipvs/ip_vs_proxy_proto.c +++ b/src/ipvs/ip_vs_proxy_proto.c @@ -423,7 +423,7 @@ int proxy_proto_insert(struct proxy_info *ppinfo, struct dp_vs_conn *conn, void *rt; int ppdoff, ppdatalen, room, mtu; int oaf; - char ppv1buf[108], tbuf1[64], tbuf2[64]; + char ppv1buf[108], tbuf1[INET6_ADDRSTRLEN], tbuf2[INET6_ADDRSTRLEN]; struct proxy_hdr_v2 *pphv2; assert(ppinfo && conn && mbuf && l4hdr); From 8286ef29ef3d817c5bc85d92c159ab94f442bc36 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Mon, 26 Aug 2024 16:58:55 +0800 Subject: [PATCH 48/63] tools/dpip: fix delay when list empty ipset with sorting enabled This problem is caused by underflow of unsigned integer. Signed-off-by: ywc689 --- tools/dpip/ipset.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/dpip/ipset.c b/tools/dpip/ipset.c index c6e954299..48d900406 100644 --- a/tools/dpip/ipset.c +++ b/tools/dpip/ipset.c @@ -1103,7 +1103,7 @@ ipset_info_dump(struct ipset_info *info, bool sort) struct ipset_member *members = (struct ipset_member*)info->members; sort_compare_func sort_compare = types[type].sort_compare; - for (i = 0; i < info->entries - 1; i++) { + for (i = 0; i + 1 < info->entries; i++) { min = i; for (j = i + 1; j < info->entries; j++) { if (sort_compare(info->af, &members[min], &members[j]) > 0) From e1703181cdc771f3a0960e878a85250516b2e22d Mon Sep 17 00:00:00 2001 From: ywc689 Date: Tue, 27 Aug 2024 17:07:05 +0800 Subject: [PATCH 49/63] dpvs: fix ipset default address family problem Signed-off-by: ywc689 --- doc/IPset.md | 2 +- src/ipset/ipset_hash_ip.c | 10 +++++----- src/ipset/ipset_hash_ipport.c | 10 +++++----- src/ipset/ipset_hash_ipportip.c | 10 +++++----- src/ipset/ipset_hash_ipportnet.c | 6 +++--- src/ipset/ipset_hash_net.c | 6 +++--- src/ipset/ipset_hash_netport.c | 6 +++--- src/ipset/ipset_hash_netportiface.c | 6 +++--- src/ipset/ipset_hash_netportnet.c | 6 +++--- src/ipset/ipset_hash_netportnetport.c | 6 +++--- tools/dpip/ipset.c | 2 +- 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/doc/IPset.md b/doc/IPset.md index 06a36b5e2..e5f4645db 100644 --- a/doc/IPset.md +++ b/doc/IPset.md @@ -459,7 +459,7 @@ bin/dpip: invalid parameter The hash:ip,port,net set type uses a hash table to store IP address, port number and IP network address triples. Both IPv4 and IPv6 address are supported. The IP address of the IP and net should be of the same family. When adding/deleting entries, ranges are allowed but is transformed to specific host IP and port entries when stored into hash table for the "ip" and "port" segments. IPv4 supports both IP range and IP CIDR, while IPv6 supports IP CIDR only. Network address with zero prefix size is not supported, and is interpreted as host prefix size, i.e., 32 for IPv4 and 128 for IPv6. Option "nomatch" can be used to set exceptions to the set when add/deleting entries. If a test is matched against with a "nomatch" entry, then the result would end with false. The port number is interpreted together with a protocol. Supported protocols include TCP, UDP, ICMP, and ICMPv6, any other protocols are interpreted as unspec type with a protocol number of zero. ```bash -# ./bin/dpip ipset create bar hash:ip,port,net +# ./bin/dpip ipset -6 create bar hash:ip,port,net # ./bin/dpip ipset add bar 2001::1,8080-8082,2002::/64 # ./bin/dpip ipset add bar 2001::1,8080-8082,2002::aaaa:bbbb:ccc0:0/108 nomatch # ./bin/dpip ipset -v list bar diff --git a/src/ipset/ipset_hash_ip.c b/src/ipset/ipset_hash_ip.c index 4e114cc5a..b0bc670fb 100644 --- a/src/ipset/ipset_hash_ip.c +++ b/src/ipset/ipset_hash_ip.c @@ -188,14 +188,14 @@ static int hash_ip_create(struct ipset *set, struct ipset_param *param) { hash_create(set, param); - if (param->option.family == AF_INET) { - set->dsize = sizeof(elem4_t); - set->hash_len = offsetof(elem4_t, comment); - set->variant = &hash_ip_variant4; - } else { + if (param->option.family == AF_INET6) { set->dsize = sizeof(elem6_t); set->hash_len = offsetof(elem6_t, comment); set->variant = &hash_ip_variant6; + } else { + set->dsize = sizeof(elem4_t); + set->hash_len = offsetof(elem4_t, comment); + set->variant = &hash_ip_variant4; } return EDPVS_OK; diff --git a/src/ipset/ipset_hash_ipport.c b/src/ipset/ipset_hash_ipport.c index f94ce34a5..ebf39a5b8 100644 --- a/src/ipset/ipset_hash_ipport.c +++ b/src/ipset/ipset_hash_ipport.c @@ -268,14 +268,14 @@ hash_ipport_create(struct ipset *set, struct ipset_param *param) { hash_create(set, param); - if (param->option.family == AF_INET) { - set->dsize = sizeof(elem4_t); - set->hash_len = offsetof(elem4_t, comment); - set->variant = &hash_ipport_variant4; - } else { + if (param->option.family == AF_INET6) { set->dsize = sizeof(elem6_t); set->hash_len = offsetof(elem6_t, comment); set->variant = &hash_ipport_variant6; + } else { + set->dsize = sizeof(elem4_t); + set->hash_len = offsetof(elem4_t, comment); + set->variant = &hash_ipport_variant4; } return EDPVS_OK; diff --git a/src/ipset/ipset_hash_ipportip.c b/src/ipset/ipset_hash_ipportip.c index 87234e4e7..64823550e 100644 --- a/src/ipset/ipset_hash_ipportip.c +++ b/src/ipset/ipset_hash_ipportip.c @@ -284,14 +284,14 @@ hash_ipportip_create(struct ipset *set, struct ipset_param *param) { hash_create(set, param); - if (param->option.family == AF_INET) { - set->dsize = sizeof(elem4_t); - set->hash_len = offsetof(elem4_t, comment); - set->variant = &hash_ipportip_variant4; - } else { + if (param->option.family == AF_INET6) { set->dsize = sizeof(elem6_t); set->hash_len = offsetof(elem6_t, comment); set->variant = &hash_ipportip_variant6; + } else { + set->dsize = sizeof(elem4_t); + set->hash_len = offsetof(elem4_t, comment); + set->variant = &hash_ipportip_variant4; } return EDPVS_OK; diff --git a/src/ipset/ipset_hash_ipportnet.c b/src/ipset/ipset_hash_ipportnet.c index 0c040e7da..78a42008d 100644 --- a/src/ipset/ipset_hash_ipportnet.c +++ b/src/ipset/ipset_hash_ipportnet.c @@ -285,10 +285,10 @@ hash_ipportnet_create(struct ipset *set, struct ipset_param *param) set->dsize = sizeof(elem_t); set->hash_len = offsetof(elem_t, comment); - if (param->option.family == AF_INET) - set->variant = &hash_ipportnet_variant4; - else + if (param->option.family == AF_INET6) set->variant = &hash_ipportnet_variant6; + else + set->variant = &hash_ipportnet_variant4; return EDPVS_OK; } diff --git a/src/ipset/ipset_hash_net.c b/src/ipset/ipset_hash_net.c index 7bbee349d..fb9084c32 100644 --- a/src/ipset/ipset_hash_net.c +++ b/src/ipset/ipset_hash_net.c @@ -219,10 +219,10 @@ hash_net_create(struct ipset *set, struct ipset_param *param) set->dsize = sizeof(elem_t); set->hash_len = offsetof(elem_t, comment); - if (param->option.family == AF_INET) - set->variant = &hash_net_variant4; - else + if (param->option.family == AF_INET6) set->variant = &hash_net_variant6; + else + set->variant = &hash_net_variant4; return EDPVS_OK; } diff --git a/src/ipset/ipset_hash_netport.c b/src/ipset/ipset_hash_netport.c index 975eef330..fb17917d4 100644 --- a/src/ipset/ipset_hash_netport.c +++ b/src/ipset/ipset_hash_netport.c @@ -269,10 +269,10 @@ hash_netport_create(struct ipset *set, struct ipset_param *param) set->dsize = sizeof(elem_t); set->hash_len = offsetof(elem_t, comment); - if (param->option.family == AF_INET) - set->variant = &hash_netport_variant4; - else + if (param->option.family == AF_INET6) set->variant = &hash_netport_variant6; + else + set->variant = &hash_netport_variant4; return EDPVS_OK; } diff --git a/src/ipset/ipset_hash_netportiface.c b/src/ipset/ipset_hash_netportiface.c index 7b15c4bb5..ef00c5d32 100644 --- a/src/ipset/ipset_hash_netportiface.c +++ b/src/ipset/ipset_hash_netportiface.c @@ -284,10 +284,10 @@ hash_netportiface_create(struct ipset *set, struct ipset_param *param) set->dsize = sizeof(elem_t); set->hash_len = offsetof(elem_t, dev); - if (param->option.family == AF_INET) - set->variant = &hash_netportiface_variant4; - else + if (param->option.family == AF_INET6) set->variant = &hash_netportiface_variant6; + else + set->variant = &hash_netportiface_variant4; return EDPVS_OK; } diff --git a/src/ipset/ipset_hash_netportnet.c b/src/ipset/ipset_hash_netportnet.c index 29dedc4f6..16b14ec55 100644 --- a/src/ipset/ipset_hash_netportnet.c +++ b/src/ipset/ipset_hash_netportnet.c @@ -287,10 +287,10 @@ hash_netportnet_create(struct ipset *set, struct ipset_param *param) set->dsize = sizeof(elem_t); set->hash_len = offsetof(elem_t, comment); - if (param->option.family == AF_INET) - set->variant = &hash_netportnet_variant4; - else + if (param->option.family == AF_INET6) set->variant = &hash_netportnet_variant6; + else + set->variant = &hash_netportnet_variant4; return EDPVS_OK; } diff --git a/src/ipset/ipset_hash_netportnetport.c b/src/ipset/ipset_hash_netportnetport.c index 5849c3d46..2cf0090a5 100644 --- a/src/ipset/ipset_hash_netportnetport.c +++ b/src/ipset/ipset_hash_netportnetport.c @@ -299,10 +299,10 @@ hash_netportnetport_create(struct ipset *set, struct ipset_param *param) set->dsize = sizeof(elem_t); set->hash_len = offsetof(elem_t, comment); - if (param->option.family == AF_INET) - set->variant = &hash_netportnetport_variant4; - else + if (param->option.family == AF_INET6) set->variant = &hash_netportnetport_variant6; + else + set->variant = &hash_netportnetport_variant4; return EDPVS_OK; } diff --git a/tools/dpip/ipset.c b/tools/dpip/ipset.c index 48d900406..8f324d2e1 100644 --- a/tools/dpip/ipset.c +++ b/tools/dpip/ipset.c @@ -125,7 +125,7 @@ ipset_help(void) " MAC := 6 bytes MAC address string literal\n" " PORT := \"[{ tcp | udp | icmp | icmp6 }:]port1[-port2]\"\n" " OPTIONS := { comment | range NET | hashsize NUM | maxelem NUM }\n" - " ADTOPTS := { comment STRING | unmatch (for add only) }\n" + " ADTOPTS := { comment STRING | nomatch (for add only) }\n" " flag := { -F(--force) | { -4 | -6 } | -v }\n" "Examples:\n" " dpip ipset create foo bitmap:ip range 192.168.0.0/16 comment\n" From f33a98f55dcb31a46cc4b7f35c4a5adb4ff8d0f4 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 9 Aug 2024 09:40:09 +0800 Subject: [PATCH 50/63] dpvs-agent: define ipset openapi Signed-off-by: ywc689 --- tools/dpvs-agent/dpvs-agent-api.yaml | 616 +++++++++++++++++++++++---- 1 file changed, 523 insertions(+), 93 deletions(-) diff --git a/tools/dpvs-agent/dpvs-agent-api.yaml b/tools/dpvs-agent/dpvs-agent-api.yaml index d466955e2..06e822220 100644 --- a/tools/dpvs-agent/dpvs-agent-api.yaml +++ b/tools/dpvs-agent/dpvs-agent-api.yaml @@ -11,33 +11,35 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +--- swagger: "2.0" -info: +info: description: "dpvs agent api" version: "1.0.0" title: "dpvs agent" host: "petstore.swagger.io" basePath: "/v2" tags: -- name: "virtualserver" - description: "virtualserver" -- name: "route" - description: "route" -- name: "laddr" - description: "laddr" -- name: "tunnel" - description: "tunnel" -- name: "inetaddr" - description: "inet addr" -- name: "white_list" - description: "white list" -- name: "black_list" - description: "black list" -- name: "arp" - description: "arp" + - name: "virtualserver" + description: "virtualserver" + - name: "route" + description: "route" + - name: "laddr" + description: "laddr" + - name: "tunnel" + description: "tunnel" + - name: "inetaddr" + description: "inet addr" + - name: "white_list" + description: "white list" + - name: "black_list" + description: "black list" + - name: "arp" + description: "arp" + - name: "ipset" + description: "ipset" schemes: -- "http" + - "http" parameters: service-id: name: VipPort @@ -129,9 +131,9 @@ parameters: in: query type: string enum: - - unset - - on - - off + - unset + - on + - off default: unset required: false link: @@ -139,9 +141,9 @@ parameters: in: query type: string enum: - - unset - - up - - down + - unset + - up + - down default: unset required: false forward2kni: @@ -149,9 +151,9 @@ parameters: in: query type: string enum: - - unset - - on - - off + - unset + - on + - off default: unset required: false version: @@ -159,6 +161,21 @@ parameters: in: query type: string required: true + ipset-name: + name: name + in: path + type: string + required: true + ipset-object: + name: object + in: path + type: string + required: true + ipset-param: + name: ipsetParam + in: body + schema: + "$ref": "#/definitions/IpsetParam" definitions: NodeServiceSnapshot: type: object @@ -283,7 +300,7 @@ definitions: properties: Spec: "$ref": "#/definitions/RealServerSpecTiny" - Stats: + Stats: "$ref": "#/definitions/ServerStats" RealServerSpecTiny: type: object @@ -326,9 +343,6 @@ definitions: "$ref": "#/definitions/NicDeviceDetail" stats: "$ref": "#/definitions/NicDeviceStats" - #extra: - # "$ref": "#/definitions/NicDeviceStats" - #NicDeviceExtra: padding NicDeviceDetail: type: object properties: @@ -441,10 +455,10 @@ definitions: type: string description: State the component is in enum: - - Ok - - Warning - - Failure - - Disabled + - Ok + - Warning + - Failure + - Disabled msg: type: string description: Human readable status/error/warning message @@ -484,7 +498,7 @@ definitions: AddrRange: type: "object" properties: - Start: + Start: type: "string" End: type: "string" @@ -642,11 +656,240 @@ definitions: - conhash Match: "$ref": "#/definitions/MatchSpec" + IpsetOption: + description: IpsetOption mirrors include/conf/ipset.h::ipset_option. + type: object + properties: + Family: + type: string + enum: + - inet + - inet6 + NoMatch: + type: boolean + default: false + Comment: + type: boolean + default: false + HashSize: + type: integer + format: uint32 + HashMaxElem: + type: integer + format: uint32 + InetAddrRange: + description: IpsetAddrRange mirrors include/conf/inet.h::inet_addr_range. + type: object + properties: + AddrMin: + type: string + pattern: '^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[a-fA-F0-9:]+)$' + AddrMax: + type: string + pattern: '^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[a-fA-F0-9:]+)$' + PortMin: + type: integer + minimum: 0 + maximum: 65535 + PortMax: + type: integer + minimum: 0 + maximum: 65535 + IpsetParam: + description: IpsetParam mirrors include/conf/ipset.h::ipset_param. + type: object + properties: + Type: + type: string + enum: + - bitmap:ip + - bitmap:ip,mac + - bitmap:port + - hash:ip + - hash:net + - hash:ip,port + - hash:net,port + - hash:net,port,iface + - hash:ip,port,ip + - hash:ip,port,net + - hash:net,port,net + - hash:net,port,net,port + Name: + type: string + minLength: 1 + maxLength: 32 # IPSET_MAXNAMELEN = 32 + Comment: + type: string + minLength: 1 + maxLength: 32 # IPSET_MAXCOMLEN = 32 + Opcode: + type: integer + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + x-enum-varnames: + - Add + - Del + - Test + - Create + - Destroy + - Flush + - List + Options: + $ref: '#/definitions/IpsetOption' + Flag: + type: integer + format: uint16 + Proto: + type: string + enum: + - tcp + - udp + - icmp + - icmp6 + Cidr: + type: integer + format: uint8 + maximum: 16 + Range: + $ref: '#/definitions/InetAddrRange' + Mac: + type: string + pattern: '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$' + Iface: + type: string + minLength: 1 + maxLength: 16 # IFNAMSIZ = 16 + Cidr2: + type: integer + format: uint8 + maximum: 65535 + Range2: + $ref: '#/definitions/InetAddrRange' + IpsetMember: + description: IpsetMember mirrors include/conf/ipset.h::ipset_member. + type: object + properties: + Comment: + type: string + minLength: 1 + maxLength: 32 # IPSET_MAXCOMLEN = 32 + Addr: + type: string + pattern: '^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[a-fA-F0-9:]+)$' + Cidr: + type: integer + format: uint8 + maximum: 16 + Proto: + type: string + enum: + - tcp + - udp + - icmp + - icmp6 + Port: + type: integer + minimum: 0 + maximum: 65535 + Mac: + type: string + pattern: '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$' + Iface: + type: string + minLength: 1 + maxLength: 16 # IFNAMSIZ = 16 + NoMatch: + type: boolean + default: false + Addr2: + type: string + pattern: '^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[a-fA-F0-9:]+)$' + Cidr2: + type: integer + format: uint8 + maximum: 16 + Port2: + type: integer + minimum: 0 + maximum: 65535 + IpsetInfo: + description: IpsetInfo mirrors include/conf/ipset.h::ipset_info. + type: object + properties: + Type: + type: string + enum: + - bitmap:ip + - bitmap:ip,mac + - bitmap:port + - hash:ip + - hash:net + - hash:ip,port + - hash:net,port + - hash:net,port,iface + - hash:ip,port,ip + - hash:ip,port,net + - hash:net,port,net + - hash:net,port,net,port + Name: + type: string + minLength: 1 + maxLength: 32 # IPSET_MAXNAMELEN = 32 + Comment: + type: boolean + default: false + BitmapRange: + $ref: '#/definitions/InetAddrRange' + BitmapCidr: + type: integer + format: uint8 + maximum: 16 + HashSize: + type: integer + format: int32 + HashMaxElem: + type: integer + format: int32 + Af: + type: string + enum: + - inet + - inet6 + Size: + type: integer + format: uint64 + Entries: + type: integer + format: int32 + References: + type: integer + format: int32 + Members: + type: array + items: + "$ref": "#/definitions/IpsetMember" + IpsetInfoArray: + description: IpsetInfoArray mirrors include/conf/ipset.h::ipset_info_array. + type: object + properties: + Count: + type: integer + format: int32 + Infos: + type: array + items: + "$ref": "#/definitions/IpsetInfo" paths: /device: get: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/stats" summary: "display all net device list" @@ -656,10 +899,9 @@ paths: schema: type: string /device/{name}/addr: - #description: dpip addr add 192.168.88.16/32 dev dpdk0.102 get: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/stats" - "$ref": "#/parameters/verbose" @@ -676,7 +918,7 @@ paths: type: string put: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/sapool" @@ -698,7 +940,7 @@ paths: type: string delete: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/sapool" - "$ref": "#/parameters/device-name" @@ -714,10 +956,9 @@ paths: schema: type: string /device/{name}/route: - #description: dpip route add 192.168.88.16/32 dev dpdk0.102 scope kni_host get: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/stats" - "$ref": "#/parameters/device-name" @@ -733,7 +974,7 @@ paths: type: string put: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/route-config" @@ -753,7 +994,7 @@ paths: type: string delete: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/route-config" @@ -770,7 +1011,7 @@ paths: /device/{name}/netlink: get: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/stats" @@ -786,7 +1027,7 @@ paths: type: string put: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" summary: "ip link set ${name} up" @@ -801,7 +1042,7 @@ paths: type: string delete: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" summary: "ip link set ${name} down" @@ -817,7 +1058,7 @@ paths: /device/{name}/netlink/addr: get: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/stats" @@ -833,12 +1074,13 @@ paths: type: string put: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/device-addr" - summary: "ip addr add 10.0.0.1/32 dev eth0: Set ip cird to linux net device" + summary: | + ip addr add 10.0.0.1/32 dev eth0: Set ip cird to linux net device responses: '200': description: Success @@ -850,11 +1092,12 @@ paths: type: string delete: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/device-addr" - summary: "ip addr del 10.0.0.1/32 dev eth0: Delete ip cird fron linux net device" + summary: | + ip addr del 10.0.0.1/32 dev eth0: Delete ip cird fron linux net device responses: '200': description: Success @@ -864,12 +1107,10 @@ paths: description: Not Found schema: type: string - #/device/{name}/cpu /device/{name}/nic: - #description: dpip link show get: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/verbose" @@ -879,20 +1120,22 @@ paths: '200': description: Success schema: - "$ref": "#/definitions/NicDeviceSpecList" + "$ref": "#/definitions/NicDeviceSpecList" '500': description: Failure schema: type: string put: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/forward2kni" - "$ref": "#/parameters/link" - "$ref": "#/parameters/promisc" - summary: "dpip link set ${nic-name} [forward2kni,link,promisc,tc-ingress,tc-egress] [on/up,off/down]" + summary: > + dpip link set ${nic-name} + [forward2kni,link,promisc,tc-ingress,tc-egress] [on/up,off/down] responses: '200': description: Success @@ -903,10 +1146,9 @@ paths: schema: type: string /device/{name}/vlan: - #description: dpip vlan add dpdk0.102 link dpdk0 id 102 get: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/stats" @@ -922,7 +1164,7 @@ paths: type: string put: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" - "$ref": "#/parameters/vlan-config" @@ -938,7 +1180,7 @@ paths: type: string delete: tags: - - "device" + - "device" parameters: - "$ref": "#/parameters/device-name" summary: "delete special net device" @@ -954,7 +1196,7 @@ paths: /vs: get: tags: - - "virtualserver" + - "virtualserver" parameters: - "$ref": "#/parameters/stats" - "$ref": "#/parameters/snapshot" @@ -973,7 +1215,7 @@ paths: /vs/{VipPort}: get: tags: - - "virtualserver" + - "virtualserver" parameters: - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/healthcheck" @@ -986,7 +1228,7 @@ paths: schema: "$ref": "#/definitions/VirtualServerList" '404': - description: Service not found + description: Service not found schema: type: string delete: @@ -1030,7 +1272,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1044,7 +1286,7 @@ paths: /vs/{VipPort}/laddr: get: tags: - - "virtualserver" + - "virtualserver" parameters: - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/service-id" @@ -1060,7 +1302,7 @@ paths: type: string put: tags: - - "virtualserver" + - "virtualserver" parameters: - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/service-id" @@ -1078,7 +1320,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1091,7 +1333,7 @@ paths: "$ref": "#/definitions/Error" delete: tags: - - "virtualserver" + - "virtualserver" parameters: - "$ref": "#/parameters/service-id" - "$ref": "#/parameters/laddr-config" @@ -1104,7 +1346,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1127,19 +1369,18 @@ paths: responses: '200': description: Success - #schema: - # "$ref": "#/definitions/RealServerExpandList" '270': - description: "the rss-config parameter is outdated, update nothing and return the latest rs info" + description: > + the rss-config parameter is outdated, + update nothing and return the latest rs info x-go-name: Unexpected schema: - # "$ref": "#/definitions/RealServerExpandList" "$ref": "#/definitions/VirtualServerSpecExpand" '460': description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1153,7 +1394,7 @@ paths: /vs/{VipPort}/rs: get: tags: - - "virtualserver" + - "virtualserver" parameters: - "$ref": "#/parameters/snapshot" - "$ref": "#/parameters/service-id" @@ -1164,7 +1405,7 @@ paths: schema: type: string '404': - description: Service not found + description: Service not found schema: type: string delete: @@ -1185,7 +1426,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1216,7 +1457,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1248,7 +1489,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1262,7 +1503,7 @@ paths: /vs/{VipPort}/deny: get: tags: - - "virtualserver" + - "virtualserver" parameters: - "$ref": "#/parameters/service-id" responses: @@ -1271,7 +1512,7 @@ paths: schema: type: string '404': - description: Service not found + description: Service not found schema: type: string delete: @@ -1292,7 +1533,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1323,7 +1564,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1354,7 +1595,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1368,7 +1609,7 @@ paths: /vs/{VipPort}/allow: get: tags: - - "virtualserver" + - "virtualserver" parameters: - "$ref": "#/parameters/service-id" responses: @@ -1377,7 +1618,7 @@ paths: schema: type: string '404': - description: Service not found + description: Service not found schema: type: string delete: @@ -1398,7 +1639,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1429,7 +1670,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1460,7 +1701,7 @@ paths: description: Invalid frontend in service configuration x-go-name: InvalidFrontend schema: - "$ref": "#/definitions/Error" + "$ref": "#/definitions/Error" '461': description: Invalid backend in service configuration x-go-name: InvalidBackend @@ -1471,3 +1712,192 @@ paths: x-go-name: Failure schema: "$ref": "#/definitions/Error" + /ipset: + get: + summary: "Get all the ipsets and members." + tags: + - "ipset" + responses: + '200': + description: Success + schema: + $ref: "#/definitions/IpsetInfoArray" + /ipset/{name}: + get: + summary: "Get a specific ipset and its members." + tags: + - "ipset" + parameters: + - "$ref": "#/parameters/ipset-name" + responses: + '200': + description: Success + schema: + $ref: "#/definitions/IpsetInfo" + '404': + description: Ipset not found + schema: + type: string + put: + summary: "Create an ipset named {name}." + tags: + - "ipset" + parameters: + - $ref: "#/parameters/ipset-name" + - $ref: "#/parameters/ipset-param" + responses: + '200': + description: Replaced + schema: + type: string + '201': + description: Created + schema: + type: string + '400': + description: Invalid ipset parameter + schema: + type: string + '404': + description: Ipset not found + schema: + type: string + '500': + description: Service not available + x-go-name: Failure + schema: + type: string + delete: + summary: "Delete the ipset named {name}." + tags: + - "ipset" + parameters: + - $ref: "#/parameters/ipset-name" + responses: + '200': + description: Deleted + schema: + type: string + '404': + description: Ipset not found + schema: + type: string + '500': + description: Service not available + x-go-name: Failure + schema: + type: string + /ipset/{name}/{object}: + get: + summary: "Check if an object in the ipset." + tags: + - "ipset" + parameters: + - $ref: "#/parameters/ipset-name" + - $ref: "#/parameters/ipset-object" + responses: + '200': + description: Succeed + schema: + type: object + properties: + Result: + type: boolean + Message: + type: string + enum: + - Match + - MisMatch + '400': + description: Invalid ipset parameter + schema: + type: string + '404': + description: Ipset not found + schema: + type: string + '500': + description: Service not available + x-go-name: Failure + schema: + type: string + /ipset/{name}/member: + post: + summary: "Add a member to the ipset." + tags: + - "ipset" + parameters: + - $ref: "#/parameters/ipset-name" + - $ref: "#/parameters/ipset-param" + responses: + '200': + description: Existed + schema: + type: string + '201': + description: Created + schema: + type: string + '400': + description: Invalid ipset parameter + schema: + type: string + '404': + description: Ipset not found + schema: + type: string + '500': + description: Service not available + x-go-name: Failure + schema: + type: string + delete: + summary: "Delete a member from the ipset." + tags: + - "ipset" + parameters: + - $ref: "#/parameters/ipset-name" + - $ref: "#/parameters/ipset-param" + responses: + '200': + description: Succeed + schema: + type: string + '400': + description: Invalid ipset parameter + schema: + type: string + '404': + description: Ipset not found + schema: + type: string + '500': + description: Service not available + x-go-name: Failure + schema: + type: string + put: + summary: "Reset the whole ipset members." + tags: + - "ipset" + parameters: + - $ref: "#/parameters/ipset-name" + - $ref: "#/parameters/ipset-param" + responses: + '200': + description: Succeed + schema: + type: string + '400': + description: Invalid ipset parameter + schema: + type: string + '404': + description: Ipset not found + schema: + type: string + '500': + description: Service not available + x-go-name: Failure + schema: + type: string From 53f16cfc89be516ccafd73cdcf2b56b920dafd6e Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 9 Aug 2024 14:41:59 +0800 Subject: [PATCH 51/63] dpvs-agent: update ipset openapi Signed-off-by: ywc689 --- tools/dpvs-agent/dpvs-agent-api.yaml | 299 ++++++++++----------------- 1 file changed, 112 insertions(+), 187 deletions(-) diff --git a/tools/dpvs-agent/dpvs-agent-api.yaml b/tools/dpvs-agent/dpvs-agent-api.yaml index 06e822220..3e7653519 100644 --- a/tools/dpvs-agent/dpvs-agent-api.yaml +++ b/tools/dpvs-agent/dpvs-agent-api.yaml @@ -166,16 +166,16 @@ parameters: in: path type: string required: true - ipset-object: - name: object - in: path - type: string - required: true ipset-param: name: ipsetParam in: body schema: - "$ref": "#/definitions/IpsetParam" + $ref: "#/definitions/IpsetInfo" + ipset-cell: + name: ipsetCell + in: body + schema: + $ref: "#/definitions/IpsetCell" definitions: NodeServiceSnapshot: type: object @@ -657,17 +657,32 @@ definitions: Match: "$ref": "#/definitions/MatchSpec" IpsetOption: - description: IpsetOption mirrors include/conf/ipset.h::ipset_option. + description: IpsetOption defines common options for ipset operations. type: object properties: - Family: - type: string - enum: - - inet - - inet6 NoMatch: + description: > + Nomatch excludes a small element range from an ipset, + which is mainly used by network-cidr based ipset. type: boolean default: false + Force: + description: > + When add members to ipset with Force set, the already existing members + are replaced sliently instead of emitting an EDPVS_EXIST error; When delete + non-existent memebers from ipset, the DPSVS_NOTEXIST error is ignored. + type: boolean + default: false + IpsetCreationOption: + description: > + IpsetCreationOption contains all available options required + in creating an ipset. + properties: + Family: + type: string + enum: + - ipv4 + - ipv6 Comment: type: boolean default: false @@ -677,61 +692,82 @@ definitions: HashMaxElem: type: integer format: uint32 - InetAddrRange: - description: IpsetAddrRange mirrors include/conf/inet.h::inet_addr_range. + Range: + description: | + vaild format: ipv4-ipv4, ipv4/pfx, ipv6/pfx, port-port + type: string + IpsetType: + type: string + enum: + - bitmap:ip + - bitmap:ip,mac + - bitmap:port + - hash:ip + - hash:net + - hash:ip,port + - hash:net,port + - hash:net,port,iface + - hash:ip,port,ip + - hash:ip,port,net + - hash:net,port,net + - hash:net,port,net,port + IpsetMember: + description: IpsetMember represents a specific entry in ipset. type: object + required: + - Entry properties: - AddrMin: + Entry: + description: | + type specific entry data, for example + * 192.168.1.0/29 (bitmap:ip) + * 192.168.88.0/24,tcp:8080-8082 (hash:net) + * 2001::1,8080-8082,2002::aaaa:bbbb:ccc0:0/108 (hash:ip,port,net) type: string - pattern: '^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[a-fA-F0-9:]+)$' - AddrMax: + Comment: type: string - pattern: '^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[a-fA-F0-9:]+)$' - PortMin: - type: integer - minimum: 0 - maximum: 65535 - PortMax: - type: integer - minimum: 0 - maximum: 65535 - IpsetParam: - description: IpsetParam mirrors include/conf/ipset.h::ipset_param. + minLength: 1 + maxLength: 32 # IPSET_MAXCOMLEN = 32 + Options: + $ref: "#/definitions/IpsetOption" + IpsetCell: + description: IpsetCell represents an indivisible granularity of ipset member. + type: object + required: + - Type + - Member + properties: + Type: + $ref: "#/definitions/IpsetType" + Member: + $ref: "#/definitions/IpsetMember" + IpsetInfo: + description: > + IpsetInfo contains all parameters and information for ipset operations. type: object + required: + - Type + - Name properties: Type: type: string - enum: - - bitmap:ip - - bitmap:ip,mac - - bitmap:port - - hash:ip - - hash:net - - hash:ip,port - - hash:net,port - - hash:net,port,iface - - hash:ip,port,ip - - hash:ip,port,net - - hash:net,port,net - - hash:net,port,net,port + $ref: "#/definitions/IpsetType" Name: type: string minLength: 1 maxLength: 32 # IPSET_MAXNAMELEN = 32 - Comment: - type: string - minLength: 1 - maxLength: 32 # IPSET_MAXCOMLEN = 32 Opcode: + description: opertaion type code type: integer + format: uint16 enum: - - 0 - 1 - 2 - 3 - 4 - 5 - 6 + - 7 x-enum-varnames: - Add - Del @@ -740,142 +776,14 @@ definitions: - Destroy - Flush - List - Options: - $ref: '#/definitions/IpsetOption' - Flag: - type: integer - format: uint16 - Proto: - type: string - enum: - - tcp - - udp - - icmp - - icmp6 - Cidr: - type: integer - format: uint8 - maximum: 16 - Range: - $ref: '#/definitions/InetAddrRange' - Mac: - type: string - pattern: '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$' - Iface: - type: string - minLength: 1 - maxLength: 16 # IFNAMSIZ = 16 - Cidr2: - type: integer - format: uint8 - maximum: 65535 - Range2: - $ref: '#/definitions/InetAddrRange' - IpsetMember: - description: IpsetMember mirrors include/conf/ipset.h::ipset_member. - type: object - properties: - Comment: - type: string - minLength: 1 - maxLength: 32 # IPSET_MAXCOMLEN = 32 - Addr: - type: string - pattern: '^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[a-fA-F0-9:]+)$' - Cidr: - type: integer - format: uint8 - maximum: 16 - Proto: - type: string - enum: - - tcp - - udp - - icmp - - icmp6 - Port: - type: integer - minimum: 0 - maximum: 65535 - Mac: - type: string - pattern: '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$' - Iface: - type: string - minLength: 1 - maxLength: 16 # IFNAMSIZ = 16 - NoMatch: - type: boolean - default: false - Addr2: - type: string - pattern: '^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[a-fA-F0-9:]+)$' - Cidr2: - type: integer - format: uint8 - maximum: 16 - Port2: - type: integer - minimum: 0 - maximum: 65535 - IpsetInfo: - description: IpsetInfo mirrors include/conf/ipset.h::ipset_info. - type: object - properties: - Type: - type: string - enum: - - bitmap:ip - - bitmap:ip,mac - - bitmap:port - - hash:ip - - hash:net - - hash:ip,port - - hash:net,port - - hash:net,port,iface - - hash:ip,port,ip - - hash:ip,port,net - - hash:net,port,net - - hash:net,port,net,port - Name: - type: string - minLength: 1 - maxLength: 32 # IPSET_MAXNAMELEN = 32 - Comment: - type: boolean - default: false - BitmapRange: - $ref: '#/definitions/InetAddrRange' - BitmapCidr: - type: integer - format: uint8 - maximum: 16 - HashSize: - type: integer - format: int32 - HashMaxElem: - type: integer - format: int32 - Af: - type: string - enum: - - inet - - inet6 - Size: - type: integer - format: uint64 + CreationOptions: + $ref: '#/definitions/IpsetCreationOption' Entries: - type: integer - format: int32 - References: - type: integer - format: int32 - Members: type: array items: - "$ref": "#/definitions/IpsetMember" + $ref: "#/definitions/IpsetMember" IpsetInfoArray: - description: IpsetInfoArray mirrors include/conf/ipset.h::ipset_info_array. + description: IpsetInfoArray contains an array of ipset. type: object properties: Count: @@ -884,7 +792,7 @@ definitions: Infos: type: array items: - "$ref": "#/definitions/IpsetInfo" + $ref: "#/definitions/IpsetInfo" paths: /device: get: @@ -1717,16 +1625,23 @@ paths: summary: "Get all the ipsets and members." tags: - "ipset" + operationId: GetAll responses: '200': description: Success schema: $ref: "#/definitions/IpsetInfoArray" + '500': + description: Service not available + x-go-name: Failure + schema: + type: string /ipset/{name}: get: summary: "Get a specific ipset and its members." tags: - "ipset" + operationId: Get parameters: - "$ref": "#/parameters/ipset-name" responses: @@ -1738,10 +1653,16 @@ paths: description: Ipset not found schema: type: string + '500': + description: Service not available + x-go-name: Failure + schema: + type: string put: summary: "Create an ipset named {name}." tags: - "ipset" + operationId: Create parameters: - $ref: "#/parameters/ipset-name" - $ref: "#/parameters/ipset-param" @@ -1771,6 +1692,7 @@ paths: summary: "Delete the ipset named {name}." tags: - "ipset" + operationId: Destroy parameters: - $ref: "#/parameters/ipset-name" responses: @@ -1787,27 +1709,27 @@ paths: x-go-name: Failure schema: type: string - /ipset/{name}/{object}: - get: + /ipset/{name}/cell: + post: summary: "Check if an object in the ipset." tags: - "ipset" + operationId: IsIn parameters: - $ref: "#/parameters/ipset-name" - - $ref: "#/parameters/ipset-object" + - $ref: "#/parameters/ipset-cell" responses: '200': description: Succeed schema: type: object + required: + - Result properties: Result: type: boolean Message: type: string - enum: - - Match - - MisMatch '400': description: Invalid ipset parameter schema: @@ -1823,9 +1745,10 @@ paths: type: string /ipset/{name}/member: post: - summary: "Add a member to the ipset." + summary: "Add members to the ipset." tags: - "ipset" + operationId: AddMember parameters: - $ref: "#/parameters/ipset-name" - $ref: "#/parameters/ipset-param" @@ -1852,9 +1775,10 @@ paths: schema: type: string delete: - summary: "Delete a member from the ipset." + summary: "Delete members from the ipset." tags: - "ipset" + operationId: DelMember parameters: - $ref: "#/parameters/ipset-name" - $ref: "#/parameters/ipset-param" @@ -1880,6 +1804,7 @@ paths: summary: "Reset the whole ipset members." tags: - "ipset" + operationId: ReplaceMember parameters: - $ref: "#/parameters/ipset-name" - $ref: "#/parameters/ipset-param" From 14d1f0c88866666f8bf49f5069312dbfc6993fb2 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 9 Aug 2024 14:42:27 +0800 Subject: [PATCH 52/63] dpvs-agent: add auto-generated codes for ipset openapi Signed-off-by: ywc689 --- tools/dpvs-agent/models/ipset_cell.go | 159 +++ .../models/ipset_creation_option.go | 118 ++ tools/dpvs-agent/models/ipset_info.go | 284 ++++ tools/dpvs-agent/models/ipset_info_array.go | 119 ++ tools/dpvs-agent/models/ipset_member.go | 151 +++ tools/dpvs-agent/models/ipset_option.go | 55 + tools/dpvs-agent/models/ipset_type.go | 108 ++ tools/dpvs-agent/restapi/embedded_spec.go | 1201 ++++++++++++++++- .../restapi/operations/dpvs_agent_api.go | 97 ++ .../restapi/operations/ipset/add_member.go | 56 + .../operations/ipset/add_member_parameters.go | 101 ++ .../operations/ipset/add_member_responses.go | 227 ++++ .../operations/ipset/add_member_urlbuilder.go | 99 ++ .../restapi/operations/ipset/create.go | 56 + .../operations/ipset/create_parameters.go | 101 ++ .../operations/ipset/create_responses.go | 227 ++++ .../operations/ipset/create_urlbuilder.go | 99 ++ .../restapi/operations/ipset/del_member.go | 56 + .../operations/ipset/del_member_parameters.go | 101 ++ .../operations/ipset/del_member_responses.go | 184 +++ .../operations/ipset/del_member_urlbuilder.go | 99 ++ .../restapi/operations/ipset/destroy.go | 56 + .../operations/ipset/destroy_parameters.go | 71 + .../operations/ipset/destroy_responses.go | 141 ++ .../operations/ipset/destroy_urlbuilder.go | 99 ++ .../restapi/operations/ipset/get.go | 56 + .../restapi/operations/ipset/get_all.go | 56 + .../operations/ipset/get_all_parameters.go | 46 + .../operations/ipset/get_all_responses.go | 102 ++ .../operations/ipset/get_all_urlbuilder.go | 87 ++ .../operations/ipset/get_parameters.go | 71 + .../restapi/operations/ipset/get_responses.go | 145 ++ .../operations/ipset/get_urlbuilder.go | 99 ++ .../restapi/operations/ipset/is_in.go | 120 ++ .../operations/ipset/is_in_parameters.go | 101 ++ .../operations/ipset/is_in_responses.go | 186 +++ .../operations/ipset/is_in_urlbuilder.go | 99 ++ .../operations/ipset/replace_member.go | 56 + .../ipset/replace_member_parameters.go | 101 ++ .../ipset/replace_member_responses.go | 184 +++ .../ipset/replace_member_urlbuilder.go | 99 ++ 41 files changed, 5611 insertions(+), 62 deletions(-) create mode 100644 tools/dpvs-agent/models/ipset_cell.go create mode 100644 tools/dpvs-agent/models/ipset_creation_option.go create mode 100644 tools/dpvs-agent/models/ipset_info.go create mode 100644 tools/dpvs-agent/models/ipset_info_array.go create mode 100644 tools/dpvs-agent/models/ipset_member.go create mode 100644 tools/dpvs-agent/models/ipset_option.go create mode 100644 tools/dpvs-agent/models/ipset_type.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/add_member.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/add_member_parameters.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/add_member_responses.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/add_member_urlbuilder.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/create.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/create_parameters.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/create_responses.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/create_urlbuilder.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/del_member.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/del_member_parameters.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/del_member_responses.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/del_member_urlbuilder.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/destroy.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/destroy_parameters.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/destroy_responses.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/destroy_urlbuilder.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/get.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/get_all.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/get_all_parameters.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/get_all_responses.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/get_all_urlbuilder.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/get_parameters.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/get_responses.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/get_urlbuilder.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/is_in.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/is_in_parameters.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/is_in_responses.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/is_in_urlbuilder.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/replace_member.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/replace_member_parameters.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/replace_member_responses.go create mode 100644 tools/dpvs-agent/restapi/operations/ipset/replace_member_urlbuilder.go diff --git a/tools/dpvs-agent/models/ipset_cell.go b/tools/dpvs-agent/models/ipset_cell.go new file mode 100644 index 000000000..280262d27 --- /dev/null +++ b/tools/dpvs-agent/models/ipset_cell.go @@ -0,0 +1,159 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// IpsetCell IpsetCell represents an indivisible granularity of ipset member. +// +// swagger:model IpsetCell +type IpsetCell struct { + + // member + // Required: true + Member *IpsetMember `json:"Member"` + + // type + // Required: true + Type *IpsetType `json:"Type"` +} + +// Validate validates this ipset cell +func (m *IpsetCell) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateMember(formats); err != nil { + res = append(res, err) + } + + if err := m.validateType(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IpsetCell) validateMember(formats strfmt.Registry) error { + + if err := validate.Required("Member", "body", m.Member); err != nil { + return err + } + + if m.Member != nil { + if err := m.Member.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Member") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Member") + } + return err + } + } + + return nil +} + +func (m *IpsetCell) validateType(formats strfmt.Registry) error { + + if err := validate.Required("Type", "body", m.Type); err != nil { + return err + } + + if err := validate.Required("Type", "body", m.Type); err != nil { + return err + } + + if m.Type != nil { + if err := m.Type.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Type") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Type") + } + return err + } + } + + return nil +} + +// ContextValidate validate this ipset cell based on the context it is used +func (m *IpsetCell) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateMember(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateType(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IpsetCell) contextValidateMember(ctx context.Context, formats strfmt.Registry) error { + + if m.Member != nil { + if err := m.Member.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Member") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Member") + } + return err + } + } + + return nil +} + +func (m *IpsetCell) contextValidateType(ctx context.Context, formats strfmt.Registry) error { + + if m.Type != nil { + if err := m.Type.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Type") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Type") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *IpsetCell) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IpsetCell) UnmarshalBinary(b []byte) error { + var res IpsetCell + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/tools/dpvs-agent/models/ipset_creation_option.go b/tools/dpvs-agent/models/ipset_creation_option.go new file mode 100644 index 000000000..ff176fb85 --- /dev/null +++ b/tools/dpvs-agent/models/ipset_creation_option.go @@ -0,0 +1,118 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// IpsetCreationOption IpsetCreationOption contains all available options required in creating an ipset. +// +// swagger:model IpsetCreationOption +type IpsetCreationOption struct { + + // comment + Comment *bool `json:"Comment,omitempty"` + + // family + // Enum: [ipv4 ipv6] + Family string `json:"Family,omitempty"` + + // hash max elem + HashMaxElem uint32 `json:"HashMaxElem,omitempty"` + + // hash size + HashSize uint32 `json:"HashSize,omitempty"` + + // vaild format: ipv4-ipv4, ipv4/pfx, ipv6/pfx, port-port + // + Range string `json:"Range,omitempty"` +} + +// Validate validates this ipset creation option +func (m *IpsetCreationOption) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateFamily(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +var ipsetCreationOptionTypeFamilyPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["ipv4","ipv6"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + ipsetCreationOptionTypeFamilyPropEnum = append(ipsetCreationOptionTypeFamilyPropEnum, v) + } +} + +const ( + + // IpsetCreationOptionFamilyIPV4 captures enum value "ipv4" + IpsetCreationOptionFamilyIPV4 string = "ipv4" + + // IpsetCreationOptionFamilyIPV6 captures enum value "ipv6" + IpsetCreationOptionFamilyIPV6 string = "ipv6" +) + +// prop value enum +func (m *IpsetCreationOption) validateFamilyEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, ipsetCreationOptionTypeFamilyPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *IpsetCreationOption) validateFamily(formats strfmt.Registry) error { + if swag.IsZero(m.Family) { // not required + return nil + } + + // value enum + if err := m.validateFamilyEnum("Family", "body", m.Family); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this ipset creation option based on context it is used +func (m *IpsetCreationOption) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *IpsetCreationOption) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IpsetCreationOption) UnmarshalBinary(b []byte) error { + var res IpsetCreationOption + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/tools/dpvs-agent/models/ipset_info.go b/tools/dpvs-agent/models/ipset_info.go new file mode 100644 index 000000000..39e6b1e2b --- /dev/null +++ b/tools/dpvs-agent/models/ipset_info.go @@ -0,0 +1,284 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "encoding/json" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// IpsetInfo IpsetInfo contains all parameters and information for ipset operations. +// +// swagger:model IpsetInfo +type IpsetInfo struct { + + // creation options + CreationOptions *IpsetCreationOption `json:"CreationOptions,omitempty"` + + // entries + Entries []*IpsetMember `json:"Entries"` + + // name + // Required: true + // Max Length: 32 + // Min Length: 1 + Name *string `json:"Name"` + + // opertaion type code + // Enum: [1 2 3 4 5 6 7] + Opcode uint16 `json:"Opcode,omitempty"` + + // type + // Required: true + Type *IpsetType `json:"Type"` +} + +// Validate validates this ipset info +func (m *IpsetInfo) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateCreationOptions(formats); err != nil { + res = append(res, err) + } + + if err := m.validateEntries(formats); err != nil { + res = append(res, err) + } + + if err := m.validateName(formats); err != nil { + res = append(res, err) + } + + if err := m.validateOpcode(formats); err != nil { + res = append(res, err) + } + + if err := m.validateType(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IpsetInfo) validateCreationOptions(formats strfmt.Registry) error { + if swag.IsZero(m.CreationOptions) { // not required + return nil + } + + if m.CreationOptions != nil { + if err := m.CreationOptions.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("CreationOptions") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("CreationOptions") + } + return err + } + } + + return nil +} + +func (m *IpsetInfo) validateEntries(formats strfmt.Registry) error { + if swag.IsZero(m.Entries) { // not required + return nil + } + + for i := 0; i < len(m.Entries); i++ { + if swag.IsZero(m.Entries[i]) { // not required + continue + } + + if m.Entries[i] != nil { + if err := m.Entries[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Entries" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Entries" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +func (m *IpsetInfo) validateName(formats strfmt.Registry) error { + + if err := validate.Required("Name", "body", m.Name); err != nil { + return err + } + + if err := validate.MinLength("Name", "body", *m.Name, 1); err != nil { + return err + } + + if err := validate.MaxLength("Name", "body", *m.Name, 32); err != nil { + return err + } + + return nil +} + +var ipsetInfoTypeOpcodePropEnum []interface{} + +func init() { + var res []uint16 + if err := json.Unmarshal([]byte(`[1,2,3,4,5,6,7]`), &res); err != nil { + panic(err) + } + for _, v := range res { + ipsetInfoTypeOpcodePropEnum = append(ipsetInfoTypeOpcodePropEnum, v) + } +} + +// prop value enum +func (m *IpsetInfo) validateOpcodeEnum(path, location string, value uint16) error { + if err := validate.EnumCase(path, location, value, ipsetInfoTypeOpcodePropEnum, true); err != nil { + return err + } + return nil +} + +func (m *IpsetInfo) validateOpcode(formats strfmt.Registry) error { + if swag.IsZero(m.Opcode) { // not required + return nil + } + + // value enum + if err := m.validateOpcodeEnum("Opcode", "body", m.Opcode); err != nil { + return err + } + + return nil +} + +func (m *IpsetInfo) validateType(formats strfmt.Registry) error { + + if err := validate.Required("Type", "body", m.Type); err != nil { + return err + } + + if err := validate.Required("Type", "body", m.Type); err != nil { + return err + } + + if m.Type != nil { + if err := m.Type.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Type") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Type") + } + return err + } + } + + return nil +} + +// ContextValidate validate this ipset info based on the context it is used +func (m *IpsetInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateCreationOptions(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateEntries(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateType(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IpsetInfo) contextValidateCreationOptions(ctx context.Context, formats strfmt.Registry) error { + + if m.CreationOptions != nil { + if err := m.CreationOptions.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("CreationOptions") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("CreationOptions") + } + return err + } + } + + return nil +} + +func (m *IpsetInfo) contextValidateEntries(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Entries); i++ { + + if m.Entries[i] != nil { + if err := m.Entries[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Entries" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Entries" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +func (m *IpsetInfo) contextValidateType(ctx context.Context, formats strfmt.Registry) error { + + if m.Type != nil { + if err := m.Type.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Type") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Type") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *IpsetInfo) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IpsetInfo) UnmarshalBinary(b []byte) error { + var res IpsetInfo + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/tools/dpvs-agent/models/ipset_info_array.go b/tools/dpvs-agent/models/ipset_info_array.go new file mode 100644 index 000000000..432e65ff3 --- /dev/null +++ b/tools/dpvs-agent/models/ipset_info_array.go @@ -0,0 +1,119 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// IpsetInfoArray IpsetInfoArray contains an array of ipset. +// +// swagger:model IpsetInfoArray +type IpsetInfoArray struct { + + // count + Count int32 `json:"Count,omitempty"` + + // infos + Infos []*IpsetInfo `json:"Infos"` +} + +// Validate validates this ipset info array +func (m *IpsetInfoArray) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateInfos(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IpsetInfoArray) validateInfos(formats strfmt.Registry) error { + if swag.IsZero(m.Infos) { // not required + return nil + } + + for i := 0; i < len(m.Infos); i++ { + if swag.IsZero(m.Infos[i]) { // not required + continue + } + + if m.Infos[i] != nil { + if err := m.Infos[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Infos" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Infos" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this ipset info array based on the context it is used +func (m *IpsetInfoArray) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateInfos(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IpsetInfoArray) contextValidateInfos(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Infos); i++ { + + if m.Infos[i] != nil { + if err := m.Infos[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Infos" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Infos" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *IpsetInfoArray) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IpsetInfoArray) UnmarshalBinary(b []byte) error { + var res IpsetInfoArray + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/tools/dpvs-agent/models/ipset_member.go b/tools/dpvs-agent/models/ipset_member.go new file mode 100644 index 000000000..c1ac8bab2 --- /dev/null +++ b/tools/dpvs-agent/models/ipset_member.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// IpsetMember IpsetMember represents a specific entry in ipset. +// +// swagger:model IpsetMember +type IpsetMember struct { + + // comment + // Max Length: 32 + // Min Length: 1 + Comment string `json:"Comment,omitempty"` + + // type specific entry data, for example + // * 192.168.1.0/29 (bitmap:ip) + // * 192.168.88.0/24,tcp:8080-8082 (hash:net) + // * 2001::1,8080-8082,2002::aaaa:bbbb:ccc0:0/108 (hash:ip,port,net) + // + // Required: true + Entry *string `json:"Entry"` + + // options + Options *IpsetOption `json:"Options,omitempty"` +} + +// Validate validates this ipset member +func (m *IpsetMember) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateComment(formats); err != nil { + res = append(res, err) + } + + if err := m.validateEntry(formats); err != nil { + res = append(res, err) + } + + if err := m.validateOptions(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IpsetMember) validateComment(formats strfmt.Registry) error { + if swag.IsZero(m.Comment) { // not required + return nil + } + + if err := validate.MinLength("Comment", "body", m.Comment, 1); err != nil { + return err + } + + if err := validate.MaxLength("Comment", "body", m.Comment, 32); err != nil { + return err + } + + return nil +} + +func (m *IpsetMember) validateEntry(formats strfmt.Registry) error { + + if err := validate.Required("Entry", "body", m.Entry); err != nil { + return err + } + + return nil +} + +func (m *IpsetMember) validateOptions(formats strfmt.Registry) error { + if swag.IsZero(m.Options) { // not required + return nil + } + + if m.Options != nil { + if err := m.Options.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Options") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Options") + } + return err + } + } + + return nil +} + +// ContextValidate validate this ipset member based on the context it is used +func (m *IpsetMember) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateOptions(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IpsetMember) contextValidateOptions(ctx context.Context, formats strfmt.Registry) error { + + if m.Options != nil { + if err := m.Options.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("Options") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("Options") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *IpsetMember) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IpsetMember) UnmarshalBinary(b []byte) error { + var res IpsetMember + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/tools/dpvs-agent/models/ipset_option.go b/tools/dpvs-agent/models/ipset_option.go new file mode 100644 index 000000000..a250729c0 --- /dev/null +++ b/tools/dpvs-agent/models/ipset_option.go @@ -0,0 +1,55 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// IpsetOption IpsetOption defines common options for ipset operations. +// +// swagger:model IpsetOption +type IpsetOption struct { + + // When add members to ipset with Force set, the already existing members are replaced sliently instead of emitting an EDPVS_EXIST error; When delete non-existent memebers from ipset, the DPSVS_NOTEXIST error is ignored. + // + Force *bool `json:"Force,omitempty"` + + // Nomatch excludes a small element range from an ipset, which is mainly used by network-cidr based ipset. + // + NoMatch *bool `json:"NoMatch,omitempty"` +} + +// Validate validates this ipset option +func (m *IpsetOption) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this ipset option based on context it is used +func (m *IpsetOption) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *IpsetOption) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IpsetOption) UnmarshalBinary(b []byte) error { + var res IpsetOption + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/tools/dpvs-agent/models/ipset_type.go b/tools/dpvs-agent/models/ipset_type.go new file mode 100644 index 000000000..ecf655aa8 --- /dev/null +++ b/tools/dpvs-agent/models/ipset_type.go @@ -0,0 +1,108 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// IpsetType ipset type +// +// swagger:model IpsetType +type IpsetType string + +func NewIpsetType(value IpsetType) *IpsetType { + return &value +} + +// Pointer returns a pointer to a freshly-allocated IpsetType. +func (m IpsetType) Pointer() *IpsetType { + return &m +} + +const ( + + // IpsetTypeBitmapIP captures enum value "bitmap:ip" + IpsetTypeBitmapIP IpsetType = "bitmap:ip" + + // IpsetTypeBitmapIPMac captures enum value "bitmap:ip,mac" + IpsetTypeBitmapIPMac IpsetType = "bitmap:ip,mac" + + // IpsetTypeBitmapPort captures enum value "bitmap:port" + IpsetTypeBitmapPort IpsetType = "bitmap:port" + + // IpsetTypeHashIP captures enum value "hash:ip" + IpsetTypeHashIP IpsetType = "hash:ip" + + // IpsetTypeHashNet captures enum value "hash:net" + IpsetTypeHashNet IpsetType = "hash:net" + + // IpsetTypeHashIPPort captures enum value "hash:ip,port" + IpsetTypeHashIPPort IpsetType = "hash:ip,port" + + // IpsetTypeHashNetPort captures enum value "hash:net,port" + IpsetTypeHashNetPort IpsetType = "hash:net,port" + + // IpsetTypeHashNetPortIface captures enum value "hash:net,port,iface" + IpsetTypeHashNetPortIface IpsetType = "hash:net,port,iface" + + // IpsetTypeHashIPPortIP captures enum value "hash:ip,port,ip" + IpsetTypeHashIPPortIP IpsetType = "hash:ip,port,ip" + + // IpsetTypeHashIPPortNet captures enum value "hash:ip,port,net" + IpsetTypeHashIPPortNet IpsetType = "hash:ip,port,net" + + // IpsetTypeHashNetPortNet captures enum value "hash:net,port,net" + IpsetTypeHashNetPortNet IpsetType = "hash:net,port,net" + + // IpsetTypeHashNetPortNetPort captures enum value "hash:net,port,net,port" + IpsetTypeHashNetPortNetPort IpsetType = "hash:net,port,net,port" +) + +// for schema +var ipsetTypeEnum []interface{} + +func init() { + var res []IpsetType + if err := json.Unmarshal([]byte(`["bitmap:ip","bitmap:ip,mac","bitmap:port","hash:ip","hash:net","hash:ip,port","hash:net,port","hash:net,port,iface","hash:ip,port,ip","hash:ip,port,net","hash:net,port,net","hash:net,port,net,port"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + ipsetTypeEnum = append(ipsetTypeEnum, v) + } +} + +func (m IpsetType) validateIpsetTypeEnum(path, location string, value IpsetType) error { + if err := validate.EnumCase(path, location, value, ipsetTypeEnum, true); err != nil { + return err + } + return nil +} + +// Validate validates this ipset type +func (m IpsetType) Validate(formats strfmt.Registry) error { + var res []error + + // value enum + if err := m.validateIpsetTypeEnum("", "body", m); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// ContextValidate validates this ipset type based on context it is used +func (m IpsetType) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/tools/dpvs-agent/restapi/embedded_spec.go b/tools/dpvs-agent/restapi/embedded_spec.go index 1e474f23c..323f0a90c 100644 --- a/tools/dpvs-agent/restapi/embedded_spec.go +++ b/tools/dpvs-agent/restapi/embedded_spec.go @@ -268,7 +268,7 @@ func init() { "tags": [ "device" ], - "summary": "ip addr add 10.0.0.1/32 dev eth0: Set ip cird to linux net device", + "summary": "ip addr add 10.0.0.1/32 dev eth0: Set ip cird to linux net device\n", "parameters": [ { "$ref": "#/parameters/snapshot" @@ -299,7 +299,7 @@ func init() { "tags": [ "device" ], - "summary": "ip addr del 10.0.0.1/32 dev eth0: Delete ip cird fron linux net device", + "summary": "ip addr del 10.0.0.1/32 dev eth0: Delete ip cird fron linux net device\n", "parameters": [ { "$ref": "#/parameters/device-name" @@ -360,7 +360,7 @@ func init() { "tags": [ "device" ], - "summary": "dpip link set ${nic-name} [forward2kni,link,promisc,tc-ingress,tc-egress] [on/up,off/down]", + "summary": "dpip link set ${nic-name} [forward2kni,link,promisc,tc-ingress,tc-egress] [on/up,off/down]\n", "parameters": [ { "$ref": "#/parameters/device-name" @@ -566,6 +566,335 @@ func init() { } } }, + "/ipset": { + "get": { + "tags": [ + "ipset" + ], + "summary": "Get all the ipsets and members.", + "operationId": "GetAll", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/IpsetInfoArray" + } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" + } + } + } + }, + "/ipset/{name}": { + "get": { + "tags": [ + "ipset" + ], + "summary": "Get a specific ipset and its members.", + "operationId": "Get", + "parameters": [ + { + "$ref": "#/parameters/ipset-name" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/IpsetInfo" + } + }, + "404": { + "description": "Ipset not found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" + } + } + }, + "put": { + "tags": [ + "ipset" + ], + "summary": "Create an ipset named {name}.", + "operationId": "Create", + "parameters": [ + { + "$ref": "#/parameters/ipset-name" + }, + { + "$ref": "#/parameters/ipset-param" + } + ], + "responses": { + "200": { + "description": "Replaced", + "schema": { + "type": "string" + } + }, + "201": { + "description": "Created", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid ipset parameter", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Ipset not found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" + } + } + }, + "delete": { + "tags": [ + "ipset" + ], + "summary": "Delete the ipset named {name}.", + "operationId": "Destroy", + "parameters": [ + { + "$ref": "#/parameters/ipset-name" + } + ], + "responses": { + "200": { + "description": "Deleted", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Ipset not found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" + } + } + } + }, + "/ipset/{name}/cell": { + "post": { + "tags": [ + "ipset" + ], + "summary": "Check if an object in the ipset.", + "operationId": "IsIn", + "parameters": [ + { + "$ref": "#/parameters/ipset-name" + }, + { + "$ref": "#/parameters/ipset-cell" + } + ], + "responses": { + "200": { + "description": "Succeed", + "schema": { + "type": "object", + "required": [ + "Result" + ], + "properties": { + "Message": { + "type": "string" + }, + "Result": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Invalid ipset parameter", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Ipset not found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" + } + } + } + }, + "/ipset/{name}/member": { + "put": { + "tags": [ + "ipset" + ], + "summary": "Reset the whole ipset members.", + "operationId": "ReplaceMember", + "parameters": [ + { + "$ref": "#/parameters/ipset-name" + }, + { + "$ref": "#/parameters/ipset-param" + } + ], + "responses": { + "200": { + "description": "Succeed", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid ipset parameter", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Ipset not found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" + } + } + }, + "post": { + "tags": [ + "ipset" + ], + "summary": "Add members to the ipset.", + "operationId": "AddMember", + "parameters": [ + { + "$ref": "#/parameters/ipset-name" + }, + { + "$ref": "#/parameters/ipset-param" + } + ], + "responses": { + "200": { + "description": "Existed", + "schema": { + "type": "string" + } + }, + "201": { + "description": "Created", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid ipset parameter", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Ipset not found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" + } + } + }, + "delete": { + "tags": [ + "ipset" + ], + "summary": "Delete members from the ipset.", + "operationId": "DelMember", + "parameters": [ + { + "$ref": "#/parameters/ipset-name" + }, + { + "$ref": "#/parameters/ipset-param" + } + ], + "responses": { + "200": { + "description": "Succeed", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid ipset parameter", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Ipset not found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" + } + } + } + }, "/vs": { "get": { "tags": [ @@ -1386,7 +1715,7 @@ func init() { "description": "Success" }, "270": { - "description": "the rss-config parameter is outdated, update nothing and return the latest rs info", + "description": "the rss-config parameter is outdated, update nothing and return the latest rs info\n", "schema": { "$ref": "#/definitions/VirtualServerSpecExpand" }, @@ -1483,11 +1812,176 @@ func init() { "broadcast": { "type": "string" }, - "scope": { - "type": "string" + "scope": { + "type": "string" + } + } + }, + "IpsetCell": { + "description": "IpsetCell represents an indivisible granularity of ipset member.", + "type": "object", + "required": [ + "Type", + "Member" + ], + "properties": { + "Member": { + "$ref": "#/definitions/IpsetMember" + }, + "Type": { + "$ref": "#/definitions/IpsetType" + } + } + }, + "IpsetCreationOption": { + "description": "IpsetCreationOption contains all available options required in creating an ipset.\n", + "properties": { + "Comment": { + "type": "boolean", + "default": false + }, + "Family": { + "type": "string", + "enum": [ + "ipv4", + "ipv6" + ] + }, + "HashMaxElem": { + "type": "integer", + "format": "uint32" + }, + "HashSize": { + "type": "integer", + "format": "uint32" + }, + "Range": { + "description": "vaild format: ipv4-ipv4, ipv4/pfx, ipv6/pfx, port-port\n", + "type": "string" + } + } + }, + "IpsetInfo": { + "description": "IpsetInfo contains all parameters and information for ipset operations.\n", + "type": "object", + "required": [ + "Type", + "Name" + ], + "properties": { + "CreationOptions": { + "$ref": "#/definitions/IpsetCreationOption" + }, + "Entries": { + "type": "array", + "items": { + "$ref": "#/definitions/IpsetMember" + } + }, + "Name": { + "type": "string", + "maxLength": 32, + "minLength": 1 + }, + "Opcode": { + "description": "opertaion type code", + "type": "integer", + "format": "uint16", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "x-enum-varnames": [ + "Add", + "Del", + "Test", + "Create", + "Destroy", + "Flush", + "List" + ] + }, + "Type": { + "type": "string", + "$ref": "#/definitions/IpsetType" + } + } + }, + "IpsetInfoArray": { + "description": "IpsetInfoArray contains an array of ipset.", + "type": "object", + "properties": { + "Count": { + "type": "integer", + "format": "int32" + }, + "Infos": { + "type": "array", + "items": { + "$ref": "#/definitions/IpsetInfo" + } + } + } + }, + "IpsetMember": { + "description": "IpsetMember represents a specific entry in ipset.", + "type": "object", + "required": [ + "Entry" + ], + "properties": { + "Comment": { + "type": "string", + "maxLength": 32, + "minLength": 1 + }, + "Entry": { + "description": "type specific entry data, for example\n* 192.168.1.0/29 (bitmap:ip)\n* 192.168.88.0/24,tcp:8080-8082 (hash:net)\n* 2001::1,8080-8082,2002::aaaa:bbbb:ccc0:0/108 (hash:ip,port,net)\n", + "type": "string" + }, + "Options": { + "$ref": "#/definitions/IpsetOption" + } + } + }, + "IpsetOption": { + "description": "IpsetOption defines common options for ipset operations.", + "type": "object", + "properties": { + "Force": { + "description": "When add members to ipset with Force set, the already existing members are replaced sliently instead of emitting an EDPVS_EXIST error; When delete non-existent memebers from ipset, the DPSVS_NOTEXIST error is ignored.\n", + "type": "boolean", + "default": false + }, + "NoMatch": { + "description": "Nomatch excludes a small element range from an ipset, which is mainly used by network-cidr based ipset.\n", + "type": "boolean", + "default": false } } }, + "IpsetType": { + "type": "string", + "enum": [ + "bitmap:ip", + "bitmap:ip,mac", + "bitmap:port", + "hash:ip", + "hash:net", + "hash:ip,port", + "hash:net,port", + "hash:net,port,iface", + "hash:ip,port,ip", + "hash:ip,port,net", + "hash:net,port,net", + "hash:net,port,net,port" + ] + }, "LocalAddressExpandList": { "properties": { "Items": { @@ -2138,6 +2632,26 @@ func init() { "name": "healthcheck", "in": "query" }, + "ipset-cell": { + "name": "ipsetCell", + "in": "body", + "schema": { + "$ref": "#/definitions/IpsetCell" + } + }, + "ipset-name": { + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + "ipset-param": { + "name": "ipsetParam", + "in": "body", + "schema": { + "$ref": "#/definitions/IpsetInfo" + } + }, "laddr-config": { "name": "spec", "in": "body", @@ -2277,6 +2791,10 @@ func init() { { "description": "arp", "name": "arp" + }, + { + "description": "ipset", + "name": "ipset" } ] }`)) @@ -2584,7 +3102,7 @@ func init() { "tags": [ "device" ], - "summary": "ip addr add 10.0.0.1/32 dev eth0: Set ip cird to linux net device", + "summary": "ip addr add 10.0.0.1/32 dev eth0: Set ip cird to linux net device\n", "parameters": [ { "type": "boolean", @@ -2625,7 +3143,7 @@ func init() { "tags": [ "device" ], - "summary": "ip addr del 10.0.0.1/32 dev eth0: Delete ip cird fron linux net device", + "summary": "ip addr del 10.0.0.1/32 dev eth0: Delete ip cird fron linux net device\n", "parameters": [ { "type": "string", @@ -2702,7 +3220,7 @@ func init() { "tags": [ "device" ], - "summary": "dpip link set ${nic-name} [forward2kni,link,promisc,tc-ingress,tc-egress] [on/up,off/down]", + "summary": "dpip link set ${nic-name} [forward2kni,link,promisc,tc-ingress,tc-egress] [on/up,off/down]\n", "parameters": [ { "type": "string", @@ -2748,31 +3266,261 @@ func init() { "200": { "description": "Success", "schema": { - "type": "string" + "type": "string" + } + }, + "500": { + "description": "Failure", + "schema": { + "type": "string" + } + } + } + } + }, + "/device/{name}/route": { + "get": { + "tags": [ + "device" + ], + "summary": "display special net device route", + "parameters": [ + { + "type": "boolean", + "default": false, + "name": "stats", + "in": "query" + }, + { + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "tags": [ + "device" + ], + "summary": "add/update special net device route", + "parameters": [ + { + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "spec", + "in": "body", + "schema": { + "$ref": "#/definitions/RouteSpec" + } + } + ], + "responses": { + "200": { + "description": "Update exist route Success", + "schema": { + "type": "string" + } + }, + "201": { + "description": "Add new route Success", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Failed", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "tags": [ + "device" + ], + "summary": "delete special net device route", + "parameters": [ + { + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "spec", + "in": "body", + "schema": { + "$ref": "#/definitions/RouteSpec" + } + } + ], + "responses": { + "200": { + "description": "delete route Success", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Failed", + "schema": { + "type": "string" + } + } + } + } + }, + "/device/{name}/vlan": { + "get": { + "tags": [ + "device" + ], + "summary": "display all net device list", + "parameters": [ + { + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "name": "stats", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "tags": [ + "device" + ], + "summary": "add/update special net device ", + "parameters": [ + { + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "spec", + "in": "body", + "schema": { + "$ref": "#/definitions/VlanSpec" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Failed", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "tags": [ + "device" + ], + "summary": "delete special net device", + "parameters": [ + { + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Failed", + "schema": { + "type": "string" + } + } + } + } + }, + "/ipset": { + "get": { + "tags": [ + "ipset" + ], + "summary": "Get all the ipsets and members.", + "operationId": "GetAll", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/IpsetInfoArray" } }, "500": { - "description": "Failure", + "description": "Service not available", "schema": { "type": "string" - } + }, + "x-go-name": "Failure" } } } }, - "/device/{name}/route": { + "/ipset/{name}": { "get": { "tags": [ - "device" + "ipset" ], - "summary": "display special net device route", + "summary": "Get a specific ipset and its members.", + "operationId": "Get", "parameters": [ - { - "type": "boolean", - "default": false, - "name": "stats", - "in": "query" - }, { "type": "string", "name": "name", @@ -2784,22 +3532,30 @@ func init() { "200": { "description": "Success", "schema": { - "type": "string" + "$ref": "#/definitions/IpsetInfo" } }, "404": { - "description": "Not Found", + "description": "Ipset not found", "schema": { "type": "string" } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" } } }, "put": { "tags": [ - "device" + "ipset" ], - "summary": "add/update special net device route", + "summary": "Create an ipset named {name}.", + "operationId": "Create", "parameters": [ { "type": "string", @@ -2808,39 +3564,91 @@ func init() { "required": true }, { - "name": "spec", + "name": "ipsetParam", "in": "body", "schema": { - "$ref": "#/definitions/RouteSpec" + "$ref": "#/definitions/IpsetInfo" } } ], "responses": { "200": { - "description": "Update exist route Success", + "description": "Replaced", "schema": { "type": "string" } }, "201": { - "description": "Add new route Success", + "description": "Created", "schema": { "type": "string" } }, - "500": { - "description": "Failed", + "400": { + "description": "Invalid ipset parameter", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Ipset not found", "schema": { "type": "string" } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" } } }, "delete": { "tags": [ - "device" + "ipset" ], - "summary": "delete special net device route", + "summary": "Delete the ipset named {name}.", + "operationId": "Destroy", + "parameters": [ + { + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Deleted", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Ipset not found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" + } + } + } + }, + "/ipset/{name}/cell": { + "post": { + "tags": [ + "ipset" + ], + "summary": "Check if an object in the ipset.", + "operationId": "IsIn", "parameters": [ { "type": "string", @@ -2849,35 +3657,60 @@ func init() { "required": true }, { - "name": "spec", + "name": "ipsetCell", "in": "body", "schema": { - "$ref": "#/definitions/RouteSpec" + "$ref": "#/definitions/IpsetCell" } } ], "responses": { "200": { - "description": "delete route Success", + "description": "Succeed", + "schema": { + "type": "object", + "required": [ + "Result" + ], + "properties": { + "Message": { + "type": "string" + }, + "Result": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Invalid ipset parameter", "schema": { "type": "string" } }, - "500": { - "description": "Failed", + "404": { + "description": "Ipset not found", "schema": { "type": "string" } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" } } } }, - "/device/{name}/vlan": { - "get": { + "/ipset/{name}/member": { + "put": { "tags": [ - "device" + "ipset" ], - "summary": "display all net device list", + "summary": "Reset the whole ipset members.", + "operationId": "ReplaceMember", "parameters": [ { "type": "string", @@ -2886,32 +3719,47 @@ func init() { "required": true }, { - "type": "boolean", - "default": false, - "name": "stats", - "in": "query" + "name": "ipsetParam", + "in": "body", + "schema": { + "$ref": "#/definitions/IpsetInfo" + } } ], "responses": { "200": { - "description": "Success", + "description": "Succeed", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid ipset parameter", "schema": { "type": "string" } }, "404": { - "description": "Not Found", + "description": "Ipset not found", "schema": { "type": "string" } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" } } }, - "put": { + "post": { "tags": [ - "device" + "ipset" ], - "summary": "add/update special net device ", + "summary": "Add members to the ipset.", + "operationId": "AddMember", "parameters": [ { "type": "string", @@ -2920,53 +3768,93 @@ func init() { "required": true }, { - "name": "spec", + "name": "ipsetParam", "in": "body", "schema": { - "$ref": "#/definitions/VlanSpec" + "$ref": "#/definitions/IpsetInfo" } } ], "responses": { "200": { - "description": "Success", + "description": "Existed", "schema": { "type": "string" } }, - "500": { - "description": "Failed", + "201": { + "description": "Created", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid ipset parameter", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Ipset not found", "schema": { "type": "string" } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" } } }, "delete": { "tags": [ - "device" + "ipset" ], - "summary": "delete special net device", + "summary": "Delete members from the ipset.", + "operationId": "DelMember", "parameters": [ { "type": "string", "name": "name", "in": "path", "required": true + }, + { + "name": "ipsetParam", + "in": "body", + "schema": { + "$ref": "#/definitions/IpsetInfo" + } } ], "responses": { "200": { - "description": "Success", + "description": "Succeed", "schema": { "type": "string" } }, - "500": { - "description": "Failed", + "400": { + "description": "Invalid ipset parameter", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Ipset not found", "schema": { "type": "string" } + }, + "500": { + "description": "Service not available", + "schema": { + "type": "string" + }, + "x-go-name": "Failure" } } } @@ -3942,7 +4830,7 @@ func init() { "description": "Success" }, "270": { - "description": "the rss-config parameter is outdated, update nothing and return the latest rs info", + "description": "the rss-config parameter is outdated, update nothing and return the latest rs info\n", "schema": { "$ref": "#/definitions/VirtualServerSpecExpand" }, @@ -4044,6 +4932,171 @@ func init() { } } }, + "IpsetCell": { + "description": "IpsetCell represents an indivisible granularity of ipset member.", + "type": "object", + "required": [ + "Type", + "Member" + ], + "properties": { + "Member": { + "$ref": "#/definitions/IpsetMember" + }, + "Type": { + "$ref": "#/definitions/IpsetType" + } + } + }, + "IpsetCreationOption": { + "description": "IpsetCreationOption contains all available options required in creating an ipset.\n", + "properties": { + "Comment": { + "type": "boolean", + "default": false + }, + "Family": { + "type": "string", + "enum": [ + "ipv4", + "ipv6" + ] + }, + "HashMaxElem": { + "type": "integer", + "format": "uint32" + }, + "HashSize": { + "type": "integer", + "format": "uint32" + }, + "Range": { + "description": "vaild format: ipv4-ipv4, ipv4/pfx, ipv6/pfx, port-port\n", + "type": "string" + } + } + }, + "IpsetInfo": { + "description": "IpsetInfo contains all parameters and information for ipset operations.\n", + "type": "object", + "required": [ + "Type", + "Name" + ], + "properties": { + "CreationOptions": { + "$ref": "#/definitions/IpsetCreationOption" + }, + "Entries": { + "type": "array", + "items": { + "$ref": "#/definitions/IpsetMember" + } + }, + "Name": { + "type": "string", + "maxLength": 32, + "minLength": 1 + }, + "Opcode": { + "description": "opertaion type code", + "type": "integer", + "format": "uint16", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "x-enum-varnames": [ + "Add", + "Del", + "Test", + "Create", + "Destroy", + "Flush", + "List" + ] + }, + "Type": { + "type": "string", + "$ref": "#/definitions/IpsetType" + } + } + }, + "IpsetInfoArray": { + "description": "IpsetInfoArray contains an array of ipset.", + "type": "object", + "properties": { + "Count": { + "type": "integer", + "format": "int32" + }, + "Infos": { + "type": "array", + "items": { + "$ref": "#/definitions/IpsetInfo" + } + } + } + }, + "IpsetMember": { + "description": "IpsetMember represents a specific entry in ipset.", + "type": "object", + "required": [ + "Entry" + ], + "properties": { + "Comment": { + "type": "string", + "maxLength": 32, + "minLength": 1 + }, + "Entry": { + "description": "type specific entry data, for example\n* 192.168.1.0/29 (bitmap:ip)\n* 192.168.88.0/24,tcp:8080-8082 (hash:net)\n* 2001::1,8080-8082,2002::aaaa:bbbb:ccc0:0/108 (hash:ip,port,net)\n", + "type": "string" + }, + "Options": { + "$ref": "#/definitions/IpsetOption" + } + } + }, + "IpsetOption": { + "description": "IpsetOption defines common options for ipset operations.", + "type": "object", + "properties": { + "Force": { + "description": "When add members to ipset with Force set, the already existing members are replaced sliently instead of emitting an EDPVS_EXIST error; When delete non-existent memebers from ipset, the DPSVS_NOTEXIST error is ignored.\n", + "type": "boolean", + "default": false + }, + "NoMatch": { + "description": "Nomatch excludes a small element range from an ipset, which is mainly used by network-cidr based ipset.\n", + "type": "boolean", + "default": false + } + } + }, + "IpsetType": { + "type": "string", + "enum": [ + "bitmap:ip", + "bitmap:ip,mac", + "bitmap:port", + "hash:ip", + "hash:net", + "hash:ip,port", + "hash:net,port", + "hash:net,port,iface", + "hash:ip,port,ip", + "hash:ip,port,net", + "hash:net,port,net", + "hash:net,port,net,port" + ] + }, "LocalAddressExpandList": { "properties": { "Items": { @@ -4694,6 +5747,26 @@ func init() { "name": "healthcheck", "in": "query" }, + "ipset-cell": { + "name": "ipsetCell", + "in": "body", + "schema": { + "$ref": "#/definitions/IpsetCell" + } + }, + "ipset-name": { + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + "ipset-param": { + "name": "ipsetParam", + "in": "body", + "schema": { + "$ref": "#/definitions/IpsetInfo" + } + }, "laddr-config": { "name": "spec", "in": "body", @@ -4833,6 +5906,10 @@ func init() { { "description": "arp", "name": "arp" + }, + { + "description": "ipset", + "name": "ipset" } ] }`)) diff --git a/tools/dpvs-agent/restapi/operations/dpvs_agent_api.go b/tools/dpvs-agent/restapi/operations/dpvs_agent_api.go index 8f292b4ba..ec6c9bf3c 100644 --- a/tools/dpvs-agent/restapi/operations/dpvs_agent_api.go +++ b/tools/dpvs-agent/restapi/operations/dpvs_agent_api.go @@ -20,6 +20,7 @@ import ( "github.com/go-openapi/swag" "github.com/dpvs-agent/restapi/operations/device" + "github.com/dpvs-agent/restapi/operations/ipset" "github.com/dpvs-agent/restapi/operations/virtualserver" ) @@ -45,6 +46,15 @@ func NewDpvsAgentAPI(spec *loads.Document) *DpvsAgentAPI { JSONProducer: runtime.JSONProducer(), + IpsetAddMemberHandler: ipset.AddMemberHandlerFunc(func(params ipset.AddMemberParams) middleware.Responder { + return middleware.NotImplemented("operation ipset.AddMember has not yet been implemented") + }), + IpsetCreateHandler: ipset.CreateHandlerFunc(func(params ipset.CreateParams) middleware.Responder { + return middleware.NotImplemented("operation ipset.Create has not yet been implemented") + }), + IpsetDelMemberHandler: ipset.DelMemberHandlerFunc(func(params ipset.DelMemberParams) middleware.Responder { + return middleware.NotImplemented("operation ipset.DelMember has not yet been implemented") + }), DeviceDeleteDeviceNameAddrHandler: device.DeleteDeviceNameAddrHandlerFunc(func(params device.DeleteDeviceNameAddrParams) middleware.Responder { return middleware.NotImplemented("operation device.DeleteDeviceNameAddr has not yet been implemented") }), @@ -75,6 +85,15 @@ func NewDpvsAgentAPI(spec *loads.Document) *DpvsAgentAPI { VirtualserverDeleteVsVipPortRsHandler: virtualserver.DeleteVsVipPortRsHandlerFunc(func(params virtualserver.DeleteVsVipPortRsParams) middleware.Responder { return middleware.NotImplemented("operation virtualserver.DeleteVsVipPortRs has not yet been implemented") }), + IpsetDestroyHandler: ipset.DestroyHandlerFunc(func(params ipset.DestroyParams) middleware.Responder { + return middleware.NotImplemented("operation ipset.Destroy has not yet been implemented") + }), + IpsetGetHandler: ipset.GetHandlerFunc(func(params ipset.GetParams) middleware.Responder { + return middleware.NotImplemented("operation ipset.Get has not yet been implemented") + }), + IpsetGetAllHandler: ipset.GetAllHandlerFunc(func(params ipset.GetAllParams) middleware.Responder { + return middleware.NotImplemented("operation ipset.GetAll has not yet been implemented") + }), DeviceGetDeviceHandler: device.GetDeviceHandlerFunc(func(params device.GetDeviceParams) middleware.Responder { return middleware.NotImplemented("operation device.GetDevice has not yet been implemented") }), @@ -114,6 +133,9 @@ func NewDpvsAgentAPI(spec *loads.Document) *DpvsAgentAPI { VirtualserverGetVsVipPortRsHandler: virtualserver.GetVsVipPortRsHandlerFunc(func(params virtualserver.GetVsVipPortRsParams) middleware.Responder { return middleware.NotImplemented("operation virtualserver.GetVsVipPortRs has not yet been implemented") }), + IpsetIsInHandler: ipset.IsInHandlerFunc(func(params ipset.IsInParams) middleware.Responder { + return middleware.NotImplemented("operation ipset.IsIn has not yet been implemented") + }), VirtualserverPostVsVipPortAllowHandler: virtualserver.PostVsVipPortAllowHandlerFunc(func(params virtualserver.PostVsVipPortAllowParams) middleware.Responder { return middleware.NotImplemented("operation virtualserver.PostVsVipPortAllow has not yet been implemented") }), @@ -159,6 +181,9 @@ func NewDpvsAgentAPI(spec *loads.Document) *DpvsAgentAPI { VirtualserverPutVsVipPortRsHealthHandler: virtualserver.PutVsVipPortRsHealthHandlerFunc(func(params virtualserver.PutVsVipPortRsHealthParams) middleware.Responder { return middleware.NotImplemented("operation virtualserver.PutVsVipPortRsHealth has not yet been implemented") }), + IpsetReplaceMemberHandler: ipset.ReplaceMemberHandlerFunc(func(params ipset.ReplaceMemberParams) middleware.Responder { + return middleware.NotImplemented("operation ipset.ReplaceMember has not yet been implemented") + }), } } @@ -195,6 +220,12 @@ type DpvsAgentAPI struct { // - application/json JSONProducer runtime.Producer + // IpsetAddMemberHandler sets the operation handler for the add member operation + IpsetAddMemberHandler ipset.AddMemberHandler + // IpsetCreateHandler sets the operation handler for the create operation + IpsetCreateHandler ipset.CreateHandler + // IpsetDelMemberHandler sets the operation handler for the del member operation + IpsetDelMemberHandler ipset.DelMemberHandler // DeviceDeleteDeviceNameAddrHandler sets the operation handler for the delete device name addr operation DeviceDeleteDeviceNameAddrHandler device.DeleteDeviceNameAddrHandler // DeviceDeleteDeviceNameNetlinkHandler sets the operation handler for the delete device name netlink operation @@ -215,6 +246,12 @@ type DpvsAgentAPI struct { VirtualserverDeleteVsVipPortLaddrHandler virtualserver.DeleteVsVipPortLaddrHandler // VirtualserverDeleteVsVipPortRsHandler sets the operation handler for the delete vs vip port rs operation VirtualserverDeleteVsVipPortRsHandler virtualserver.DeleteVsVipPortRsHandler + // IpsetDestroyHandler sets the operation handler for the destroy operation + IpsetDestroyHandler ipset.DestroyHandler + // IpsetGetHandler sets the operation handler for the get operation + IpsetGetHandler ipset.GetHandler + // IpsetGetAllHandler sets the operation handler for the get all operation + IpsetGetAllHandler ipset.GetAllHandler // DeviceGetDeviceHandler sets the operation handler for the get device operation DeviceGetDeviceHandler device.GetDeviceHandler // DeviceGetDeviceNameAddrHandler sets the operation handler for the get device name addr operation @@ -241,6 +278,8 @@ type DpvsAgentAPI struct { VirtualserverGetVsVipPortLaddrHandler virtualserver.GetVsVipPortLaddrHandler // VirtualserverGetVsVipPortRsHandler sets the operation handler for the get vs vip port rs operation VirtualserverGetVsVipPortRsHandler virtualserver.GetVsVipPortRsHandler + // IpsetIsInHandler sets the operation handler for the is in operation + IpsetIsInHandler ipset.IsInHandler // VirtualserverPostVsVipPortAllowHandler sets the operation handler for the post vs vip port allow operation VirtualserverPostVsVipPortAllowHandler virtualserver.PostVsVipPortAllowHandler // VirtualserverPostVsVipPortDenyHandler sets the operation handler for the post vs vip port deny operation @@ -271,6 +310,8 @@ type DpvsAgentAPI struct { VirtualserverPutVsVipPortRsHandler virtualserver.PutVsVipPortRsHandler // VirtualserverPutVsVipPortRsHealthHandler sets the operation handler for the put vs vip port rs health operation VirtualserverPutVsVipPortRsHealthHandler virtualserver.PutVsVipPortRsHealthHandler + // IpsetReplaceMemberHandler sets the operation handler for the replace member operation + IpsetReplaceMemberHandler ipset.ReplaceMemberHandler // ServeError is called when an error is received, there is a default handler // but you can set your own with this @@ -348,6 +389,15 @@ func (o *DpvsAgentAPI) Validate() error { unregistered = append(unregistered, "JSONProducer") } + if o.IpsetAddMemberHandler == nil { + unregistered = append(unregistered, "ipset.AddMemberHandler") + } + if o.IpsetCreateHandler == nil { + unregistered = append(unregistered, "ipset.CreateHandler") + } + if o.IpsetDelMemberHandler == nil { + unregistered = append(unregistered, "ipset.DelMemberHandler") + } if o.DeviceDeleteDeviceNameAddrHandler == nil { unregistered = append(unregistered, "device.DeleteDeviceNameAddrHandler") } @@ -378,6 +428,15 @@ func (o *DpvsAgentAPI) Validate() error { if o.VirtualserverDeleteVsVipPortRsHandler == nil { unregistered = append(unregistered, "virtualserver.DeleteVsVipPortRsHandler") } + if o.IpsetDestroyHandler == nil { + unregistered = append(unregistered, "ipset.DestroyHandler") + } + if o.IpsetGetHandler == nil { + unregistered = append(unregistered, "ipset.GetHandler") + } + if o.IpsetGetAllHandler == nil { + unregistered = append(unregistered, "ipset.GetAllHandler") + } if o.DeviceGetDeviceHandler == nil { unregistered = append(unregistered, "device.GetDeviceHandler") } @@ -417,6 +476,9 @@ func (o *DpvsAgentAPI) Validate() error { if o.VirtualserverGetVsVipPortRsHandler == nil { unregistered = append(unregistered, "virtualserver.GetVsVipPortRsHandler") } + if o.IpsetIsInHandler == nil { + unregistered = append(unregistered, "ipset.IsInHandler") + } if o.VirtualserverPostVsVipPortAllowHandler == nil { unregistered = append(unregistered, "virtualserver.PostVsVipPortAllowHandler") } @@ -462,6 +524,9 @@ func (o *DpvsAgentAPI) Validate() error { if o.VirtualserverPutVsVipPortRsHealthHandler == nil { unregistered = append(unregistered, "virtualserver.PutVsVipPortRsHealthHandler") } + if o.IpsetReplaceMemberHandler == nil { + unregistered = append(unregistered, "ipset.ReplaceMemberHandler") + } if len(unregistered) > 0 { return fmt.Errorf("missing registration: %s", strings.Join(unregistered, ", ")) @@ -550,6 +615,18 @@ func (o *DpvsAgentAPI) initHandlerCache() { o.handlers = make(map[string]map[string]http.Handler) } + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } + o.handlers["POST"]["/ipset/{name}/member"] = ipset.NewAddMember(o.context, o.IpsetAddMemberHandler) + if o.handlers["PUT"] == nil { + o.handlers["PUT"] = make(map[string]http.Handler) + } + o.handlers["PUT"]["/ipset/{name}"] = ipset.NewCreate(o.context, o.IpsetCreateHandler) + if o.handlers["DELETE"] == nil { + o.handlers["DELETE"] = make(map[string]http.Handler) + } + o.handlers["DELETE"]["/ipset/{name}/member"] = ipset.NewDelMember(o.context, o.IpsetDelMemberHandler) if o.handlers["DELETE"] == nil { o.handlers["DELETE"] = make(map[string]http.Handler) } @@ -590,6 +667,18 @@ func (o *DpvsAgentAPI) initHandlerCache() { o.handlers["DELETE"] = make(map[string]http.Handler) } o.handlers["DELETE"]["/vs/{VipPort}/rs"] = virtualserver.NewDeleteVsVipPortRs(o.context, o.VirtualserverDeleteVsVipPortRsHandler) + if o.handlers["DELETE"] == nil { + o.handlers["DELETE"] = make(map[string]http.Handler) + } + o.handlers["DELETE"]["/ipset/{name}"] = ipset.NewDestroy(o.context, o.IpsetDestroyHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } + o.handlers["GET"]["/ipset/{name}"] = ipset.NewGet(o.context, o.IpsetGetHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } + o.handlers["GET"]["/ipset"] = ipset.NewGetAll(o.context, o.IpsetGetAllHandler) if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } @@ -645,6 +734,10 @@ func (o *DpvsAgentAPI) initHandlerCache() { if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) } + o.handlers["POST"]["/ipset/{name}/cell"] = ipset.NewIsIn(o.context, o.IpsetIsInHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } o.handlers["POST"]["/vs/{VipPort}/allow"] = virtualserver.NewPostVsVipPortAllow(o.context, o.VirtualserverPostVsVipPortAllowHandler) if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) @@ -702,6 +795,10 @@ func (o *DpvsAgentAPI) initHandlerCache() { o.handlers["PUT"] = make(map[string]http.Handler) } o.handlers["PUT"]["/vs/{VipPort}/rs/health"] = virtualserver.NewPutVsVipPortRsHealth(o.context, o.VirtualserverPutVsVipPortRsHealthHandler) + if o.handlers["PUT"] == nil { + o.handlers["PUT"] = make(map[string]http.Handler) + } + o.handlers["PUT"]["/ipset/{name}/member"] = ipset.NewReplaceMember(o.context, o.IpsetReplaceMemberHandler) } // Serve creates a http handler to serve the API over HTTP diff --git a/tools/dpvs-agent/restapi/operations/ipset/add_member.go b/tools/dpvs-agent/restapi/operations/ipset/add_member.go new file mode 100644 index 000000000..58948ac0f --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/add_member.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// AddMemberHandlerFunc turns a function with the right signature into a add member handler +type AddMemberHandlerFunc func(AddMemberParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn AddMemberHandlerFunc) Handle(params AddMemberParams) middleware.Responder { + return fn(params) +} + +// AddMemberHandler interface for that can handle valid add member params +type AddMemberHandler interface { + Handle(AddMemberParams) middleware.Responder +} + +// NewAddMember creates a new http.Handler for the add member operation +func NewAddMember(ctx *middleware.Context, handler AddMemberHandler) *AddMember { + return &AddMember{Context: ctx, Handler: handler} +} + +/* + AddMember swagger:route POST /ipset/{name}/member ipset addMember + +Add members to the ipset. +*/ +type AddMember struct { + Context *middleware.Context + Handler AddMemberHandler +} + +func (o *AddMember) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewAddMemberParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/add_member_parameters.go b/tools/dpvs-agent/restapi/operations/ipset/add_member_parameters.go new file mode 100644 index 000000000..b2b76bc29 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/add_member_parameters.go @@ -0,0 +1,101 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + + "github.com/dpvs-agent/models" +) + +// NewAddMemberParams creates a new AddMemberParams object +// +// There are no default values defined in the spec. +func NewAddMemberParams() AddMemberParams { + + return AddMemberParams{} +} + +// AddMemberParams contains all the bound params for the add member operation +// typically these are obtained from a http.Request +// +// swagger:parameters AddMember +type AddMemberParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + In: body + */ + IpsetParam *models.IpsetInfo + /* + Required: true + In: path + */ + Name string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewAddMemberParams() beforehand. +func (o *AddMemberParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.IpsetInfo + if err := route.Consumer.Consume(r.Body, &body); err != nil { + res = append(res, errors.NewParseError("ipsetParam", "body", "", err)) + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.IpsetParam = &body + } + } + } + + rName, rhkName, _ := route.Params.GetOK("name") + if err := o.bindName(rName, rhkName, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindName binds and validates parameter Name from path. +func (o *AddMemberParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Name = raw + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/add_member_responses.go b/tools/dpvs-agent/restapi/operations/ipset/add_member_responses.go new file mode 100644 index 000000000..89aba02da --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/add_member_responses.go @@ -0,0 +1,227 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" +) + +// AddMemberOKCode is the HTTP code returned for type AddMemberOK +const AddMemberOKCode int = 200 + +/* +AddMemberOK Existed + +swagger:response addMemberOK +*/ +type AddMemberOK struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewAddMemberOK creates AddMemberOK with default headers values +func NewAddMemberOK() *AddMemberOK { + + return &AddMemberOK{} +} + +// WithPayload adds the payload to the add member o k response +func (o *AddMemberOK) WithPayload(payload string) *AddMemberOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add member o k response +func (o *AddMemberOK) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddMemberOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// AddMemberCreatedCode is the HTTP code returned for type AddMemberCreated +const AddMemberCreatedCode int = 201 + +/* +AddMemberCreated Created + +swagger:response addMemberCreated +*/ +type AddMemberCreated struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewAddMemberCreated creates AddMemberCreated with default headers values +func NewAddMemberCreated() *AddMemberCreated { + + return &AddMemberCreated{} +} + +// WithPayload adds the payload to the add member created response +func (o *AddMemberCreated) WithPayload(payload string) *AddMemberCreated { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add member created response +func (o *AddMemberCreated) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddMemberCreated) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(201) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// AddMemberBadRequestCode is the HTTP code returned for type AddMemberBadRequest +const AddMemberBadRequestCode int = 400 + +/* +AddMemberBadRequest Invalid ipset parameter + +swagger:response addMemberBadRequest +*/ +type AddMemberBadRequest struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewAddMemberBadRequest creates AddMemberBadRequest with default headers values +func NewAddMemberBadRequest() *AddMemberBadRequest { + + return &AddMemberBadRequest{} +} + +// WithPayload adds the payload to the add member bad request response +func (o *AddMemberBadRequest) WithPayload(payload string) *AddMemberBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add member bad request response +func (o *AddMemberBadRequest) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddMemberBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// AddMemberNotFoundCode is the HTTP code returned for type AddMemberNotFound +const AddMemberNotFoundCode int = 404 + +/* +AddMemberNotFound Ipset not found + +swagger:response addMemberNotFound +*/ +type AddMemberNotFound struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewAddMemberNotFound creates AddMemberNotFound with default headers values +func NewAddMemberNotFound() *AddMemberNotFound { + + return &AddMemberNotFound{} +} + +// WithPayload adds the payload to the add member not found response +func (o *AddMemberNotFound) WithPayload(payload string) *AddMemberNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add member not found response +func (o *AddMemberNotFound) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddMemberNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// AddMemberFailureCode is the HTTP code returned for type AddMemberFailure +const AddMemberFailureCode int = 500 + +/* +AddMemberFailure Service not available + +swagger:response addMemberFailure +*/ +type AddMemberFailure struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewAddMemberFailure creates AddMemberFailure with default headers values +func NewAddMemberFailure() *AddMemberFailure { + + return &AddMemberFailure{} +} + +// WithPayload adds the payload to the add member failure response +func (o *AddMemberFailure) WithPayload(payload string) *AddMemberFailure { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add member failure response +func (o *AddMemberFailure) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddMemberFailure) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/add_member_urlbuilder.go b/tools/dpvs-agent/restapi/operations/ipset/add_member_urlbuilder.go new file mode 100644 index 000000000..25bc040c4 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/add_member_urlbuilder.go @@ -0,0 +1,99 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// AddMemberURL generates an URL for the add member operation +type AddMemberURL struct { + Name string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *AddMemberURL) WithBasePath(bp string) *AddMemberURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *AddMemberURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *AddMemberURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/ipset/{name}/member" + + name := o.Name + if name != "" { + _path = strings.Replace(_path, "{name}", name, -1) + } else { + return nil, errors.New("name is required on AddMemberURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/v2" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *AddMemberURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *AddMemberURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *AddMemberURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on AddMemberURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on AddMemberURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *AddMemberURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/create.go b/tools/dpvs-agent/restapi/operations/ipset/create.go new file mode 100644 index 000000000..211fa29e0 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/create.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// CreateHandlerFunc turns a function with the right signature into a create handler +type CreateHandlerFunc func(CreateParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn CreateHandlerFunc) Handle(params CreateParams) middleware.Responder { + return fn(params) +} + +// CreateHandler interface for that can handle valid create params +type CreateHandler interface { + Handle(CreateParams) middleware.Responder +} + +// NewCreate creates a new http.Handler for the create operation +func NewCreate(ctx *middleware.Context, handler CreateHandler) *Create { + return &Create{Context: ctx, Handler: handler} +} + +/* + Create swagger:route PUT /ipset/{name} ipset create + +Create an ipset named {name}. +*/ +type Create struct { + Context *middleware.Context + Handler CreateHandler +} + +func (o *Create) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewCreateParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/create_parameters.go b/tools/dpvs-agent/restapi/operations/ipset/create_parameters.go new file mode 100644 index 000000000..7aca005bf --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/create_parameters.go @@ -0,0 +1,101 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + + "github.com/dpvs-agent/models" +) + +// NewCreateParams creates a new CreateParams object +// +// There are no default values defined in the spec. +func NewCreateParams() CreateParams { + + return CreateParams{} +} + +// CreateParams contains all the bound params for the create operation +// typically these are obtained from a http.Request +// +// swagger:parameters Create +type CreateParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + In: body + */ + IpsetParam *models.IpsetInfo + /* + Required: true + In: path + */ + Name string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewCreateParams() beforehand. +func (o *CreateParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.IpsetInfo + if err := route.Consumer.Consume(r.Body, &body); err != nil { + res = append(res, errors.NewParseError("ipsetParam", "body", "", err)) + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.IpsetParam = &body + } + } + } + + rName, rhkName, _ := route.Params.GetOK("name") + if err := o.bindName(rName, rhkName, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindName binds and validates parameter Name from path. +func (o *CreateParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Name = raw + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/create_responses.go b/tools/dpvs-agent/restapi/operations/ipset/create_responses.go new file mode 100644 index 000000000..7bd038576 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/create_responses.go @@ -0,0 +1,227 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" +) + +// CreateOKCode is the HTTP code returned for type CreateOK +const CreateOKCode int = 200 + +/* +CreateOK Replaced + +swagger:response createOK +*/ +type CreateOK struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewCreateOK creates CreateOK with default headers values +func NewCreateOK() *CreateOK { + + return &CreateOK{} +} + +// WithPayload adds the payload to the create o k response +func (o *CreateOK) WithPayload(payload string) *CreateOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the create o k response +func (o *CreateOK) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *CreateOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// CreateCreatedCode is the HTTP code returned for type CreateCreated +const CreateCreatedCode int = 201 + +/* +CreateCreated Created + +swagger:response createCreated +*/ +type CreateCreated struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewCreateCreated creates CreateCreated with default headers values +func NewCreateCreated() *CreateCreated { + + return &CreateCreated{} +} + +// WithPayload adds the payload to the create created response +func (o *CreateCreated) WithPayload(payload string) *CreateCreated { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the create created response +func (o *CreateCreated) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *CreateCreated) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(201) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// CreateBadRequestCode is the HTTP code returned for type CreateBadRequest +const CreateBadRequestCode int = 400 + +/* +CreateBadRequest Invalid ipset parameter + +swagger:response createBadRequest +*/ +type CreateBadRequest struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewCreateBadRequest creates CreateBadRequest with default headers values +func NewCreateBadRequest() *CreateBadRequest { + + return &CreateBadRequest{} +} + +// WithPayload adds the payload to the create bad request response +func (o *CreateBadRequest) WithPayload(payload string) *CreateBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the create bad request response +func (o *CreateBadRequest) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *CreateBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// CreateNotFoundCode is the HTTP code returned for type CreateNotFound +const CreateNotFoundCode int = 404 + +/* +CreateNotFound Ipset not found + +swagger:response createNotFound +*/ +type CreateNotFound struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewCreateNotFound creates CreateNotFound with default headers values +func NewCreateNotFound() *CreateNotFound { + + return &CreateNotFound{} +} + +// WithPayload adds the payload to the create not found response +func (o *CreateNotFound) WithPayload(payload string) *CreateNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the create not found response +func (o *CreateNotFound) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *CreateNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// CreateFailureCode is the HTTP code returned for type CreateFailure +const CreateFailureCode int = 500 + +/* +CreateFailure Service not available + +swagger:response createFailure +*/ +type CreateFailure struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewCreateFailure creates CreateFailure with default headers values +func NewCreateFailure() *CreateFailure { + + return &CreateFailure{} +} + +// WithPayload adds the payload to the create failure response +func (o *CreateFailure) WithPayload(payload string) *CreateFailure { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the create failure response +func (o *CreateFailure) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *CreateFailure) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/create_urlbuilder.go b/tools/dpvs-agent/restapi/operations/ipset/create_urlbuilder.go new file mode 100644 index 000000000..fe549f1ab --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/create_urlbuilder.go @@ -0,0 +1,99 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// CreateURL generates an URL for the create operation +type CreateURL struct { + Name string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *CreateURL) WithBasePath(bp string) *CreateURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *CreateURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *CreateURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/ipset/{name}" + + name := o.Name + if name != "" { + _path = strings.Replace(_path, "{name}", name, -1) + } else { + return nil, errors.New("name is required on CreateURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/v2" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *CreateURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *CreateURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *CreateURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on CreateURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on CreateURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *CreateURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/del_member.go b/tools/dpvs-agent/restapi/operations/ipset/del_member.go new file mode 100644 index 000000000..c72e13706 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/del_member.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// DelMemberHandlerFunc turns a function with the right signature into a del member handler +type DelMemberHandlerFunc func(DelMemberParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn DelMemberHandlerFunc) Handle(params DelMemberParams) middleware.Responder { + return fn(params) +} + +// DelMemberHandler interface for that can handle valid del member params +type DelMemberHandler interface { + Handle(DelMemberParams) middleware.Responder +} + +// NewDelMember creates a new http.Handler for the del member operation +func NewDelMember(ctx *middleware.Context, handler DelMemberHandler) *DelMember { + return &DelMember{Context: ctx, Handler: handler} +} + +/* + DelMember swagger:route DELETE /ipset/{name}/member ipset delMember + +Delete members from the ipset. +*/ +type DelMember struct { + Context *middleware.Context + Handler DelMemberHandler +} + +func (o *DelMember) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewDelMemberParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/del_member_parameters.go b/tools/dpvs-agent/restapi/operations/ipset/del_member_parameters.go new file mode 100644 index 000000000..58ca3b276 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/del_member_parameters.go @@ -0,0 +1,101 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + + "github.com/dpvs-agent/models" +) + +// NewDelMemberParams creates a new DelMemberParams object +// +// There are no default values defined in the spec. +func NewDelMemberParams() DelMemberParams { + + return DelMemberParams{} +} + +// DelMemberParams contains all the bound params for the del member operation +// typically these are obtained from a http.Request +// +// swagger:parameters DelMember +type DelMemberParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + In: body + */ + IpsetParam *models.IpsetInfo + /* + Required: true + In: path + */ + Name string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewDelMemberParams() beforehand. +func (o *DelMemberParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.IpsetInfo + if err := route.Consumer.Consume(r.Body, &body); err != nil { + res = append(res, errors.NewParseError("ipsetParam", "body", "", err)) + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.IpsetParam = &body + } + } + } + + rName, rhkName, _ := route.Params.GetOK("name") + if err := o.bindName(rName, rhkName, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindName binds and validates parameter Name from path. +func (o *DelMemberParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Name = raw + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/del_member_responses.go b/tools/dpvs-agent/restapi/operations/ipset/del_member_responses.go new file mode 100644 index 000000000..34767169d --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/del_member_responses.go @@ -0,0 +1,184 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" +) + +// DelMemberOKCode is the HTTP code returned for type DelMemberOK +const DelMemberOKCode int = 200 + +/* +DelMemberOK Succeed + +swagger:response delMemberOK +*/ +type DelMemberOK struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewDelMemberOK creates DelMemberOK with default headers values +func NewDelMemberOK() *DelMemberOK { + + return &DelMemberOK{} +} + +// WithPayload adds the payload to the del member o k response +func (o *DelMemberOK) WithPayload(payload string) *DelMemberOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the del member o k response +func (o *DelMemberOK) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DelMemberOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// DelMemberBadRequestCode is the HTTP code returned for type DelMemberBadRequest +const DelMemberBadRequestCode int = 400 + +/* +DelMemberBadRequest Invalid ipset parameter + +swagger:response delMemberBadRequest +*/ +type DelMemberBadRequest struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewDelMemberBadRequest creates DelMemberBadRequest with default headers values +func NewDelMemberBadRequest() *DelMemberBadRequest { + + return &DelMemberBadRequest{} +} + +// WithPayload adds the payload to the del member bad request response +func (o *DelMemberBadRequest) WithPayload(payload string) *DelMemberBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the del member bad request response +func (o *DelMemberBadRequest) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DelMemberBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// DelMemberNotFoundCode is the HTTP code returned for type DelMemberNotFound +const DelMemberNotFoundCode int = 404 + +/* +DelMemberNotFound Ipset not found + +swagger:response delMemberNotFound +*/ +type DelMemberNotFound struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewDelMemberNotFound creates DelMemberNotFound with default headers values +func NewDelMemberNotFound() *DelMemberNotFound { + + return &DelMemberNotFound{} +} + +// WithPayload adds the payload to the del member not found response +func (o *DelMemberNotFound) WithPayload(payload string) *DelMemberNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the del member not found response +func (o *DelMemberNotFound) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DelMemberNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// DelMemberFailureCode is the HTTP code returned for type DelMemberFailure +const DelMemberFailureCode int = 500 + +/* +DelMemberFailure Service not available + +swagger:response delMemberFailure +*/ +type DelMemberFailure struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewDelMemberFailure creates DelMemberFailure with default headers values +func NewDelMemberFailure() *DelMemberFailure { + + return &DelMemberFailure{} +} + +// WithPayload adds the payload to the del member failure response +func (o *DelMemberFailure) WithPayload(payload string) *DelMemberFailure { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the del member failure response +func (o *DelMemberFailure) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DelMemberFailure) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/del_member_urlbuilder.go b/tools/dpvs-agent/restapi/operations/ipset/del_member_urlbuilder.go new file mode 100644 index 000000000..8da232e0b --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/del_member_urlbuilder.go @@ -0,0 +1,99 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// DelMemberURL generates an URL for the del member operation +type DelMemberURL struct { + Name string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DelMemberURL) WithBasePath(bp string) *DelMemberURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DelMemberURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *DelMemberURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/ipset/{name}/member" + + name := o.Name + if name != "" { + _path = strings.Replace(_path, "{name}", name, -1) + } else { + return nil, errors.New("name is required on DelMemberURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/v2" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *DelMemberURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *DelMemberURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *DelMemberURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on DelMemberURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on DelMemberURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *DelMemberURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/destroy.go b/tools/dpvs-agent/restapi/operations/ipset/destroy.go new file mode 100644 index 000000000..6f8c38afd --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/destroy.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// DestroyHandlerFunc turns a function with the right signature into a destroy handler +type DestroyHandlerFunc func(DestroyParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn DestroyHandlerFunc) Handle(params DestroyParams) middleware.Responder { + return fn(params) +} + +// DestroyHandler interface for that can handle valid destroy params +type DestroyHandler interface { + Handle(DestroyParams) middleware.Responder +} + +// NewDestroy creates a new http.Handler for the destroy operation +func NewDestroy(ctx *middleware.Context, handler DestroyHandler) *Destroy { + return &Destroy{Context: ctx, Handler: handler} +} + +/* + Destroy swagger:route DELETE /ipset/{name} ipset destroy + +Delete the ipset named {name}. +*/ +type Destroy struct { + Context *middleware.Context + Handler DestroyHandler +} + +func (o *Destroy) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewDestroyParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/destroy_parameters.go b/tools/dpvs-agent/restapi/operations/ipset/destroy_parameters.go new file mode 100644 index 000000000..738d1688f --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/destroy_parameters.go @@ -0,0 +1,71 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" +) + +// NewDestroyParams creates a new DestroyParams object +// +// There are no default values defined in the spec. +func NewDestroyParams() DestroyParams { + + return DestroyParams{} +} + +// DestroyParams contains all the bound params for the destroy operation +// typically these are obtained from a http.Request +// +// swagger:parameters Destroy +type DestroyParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: path + */ + Name string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewDestroyParams() beforehand. +func (o *DestroyParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + rName, rhkName, _ := route.Params.GetOK("name") + if err := o.bindName(rName, rhkName, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindName binds and validates parameter Name from path. +func (o *DestroyParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Name = raw + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/destroy_responses.go b/tools/dpvs-agent/restapi/operations/ipset/destroy_responses.go new file mode 100644 index 000000000..4499b32c6 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/destroy_responses.go @@ -0,0 +1,141 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" +) + +// DestroyOKCode is the HTTP code returned for type DestroyOK +const DestroyOKCode int = 200 + +/* +DestroyOK Deleted + +swagger:response destroyOK +*/ +type DestroyOK struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewDestroyOK creates DestroyOK with default headers values +func NewDestroyOK() *DestroyOK { + + return &DestroyOK{} +} + +// WithPayload adds the payload to the destroy o k response +func (o *DestroyOK) WithPayload(payload string) *DestroyOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the destroy o k response +func (o *DestroyOK) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DestroyOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// DestroyNotFoundCode is the HTTP code returned for type DestroyNotFound +const DestroyNotFoundCode int = 404 + +/* +DestroyNotFound Ipset not found + +swagger:response destroyNotFound +*/ +type DestroyNotFound struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewDestroyNotFound creates DestroyNotFound with default headers values +func NewDestroyNotFound() *DestroyNotFound { + + return &DestroyNotFound{} +} + +// WithPayload adds the payload to the destroy not found response +func (o *DestroyNotFound) WithPayload(payload string) *DestroyNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the destroy not found response +func (o *DestroyNotFound) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DestroyNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// DestroyFailureCode is the HTTP code returned for type DestroyFailure +const DestroyFailureCode int = 500 + +/* +DestroyFailure Service not available + +swagger:response destroyFailure +*/ +type DestroyFailure struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewDestroyFailure creates DestroyFailure with default headers values +func NewDestroyFailure() *DestroyFailure { + + return &DestroyFailure{} +} + +// WithPayload adds the payload to the destroy failure response +func (o *DestroyFailure) WithPayload(payload string) *DestroyFailure { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the destroy failure response +func (o *DestroyFailure) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DestroyFailure) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/destroy_urlbuilder.go b/tools/dpvs-agent/restapi/operations/ipset/destroy_urlbuilder.go new file mode 100644 index 000000000..fd259b65f --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/destroy_urlbuilder.go @@ -0,0 +1,99 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// DestroyURL generates an URL for the destroy operation +type DestroyURL struct { + Name string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DestroyURL) WithBasePath(bp string) *DestroyURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DestroyURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *DestroyURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/ipset/{name}" + + name := o.Name + if name != "" { + _path = strings.Replace(_path, "{name}", name, -1) + } else { + return nil, errors.New("name is required on DestroyURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/v2" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *DestroyURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *DestroyURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *DestroyURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on DestroyURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on DestroyURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *DestroyURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/get.go b/tools/dpvs-agent/restapi/operations/ipset/get.go new file mode 100644 index 000000000..35d31e50e --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/get.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// GetHandlerFunc turns a function with the right signature into a get handler +type GetHandlerFunc func(GetParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn GetHandlerFunc) Handle(params GetParams) middleware.Responder { + return fn(params) +} + +// GetHandler interface for that can handle valid get params +type GetHandler interface { + Handle(GetParams) middleware.Responder +} + +// NewGet creates a new http.Handler for the get operation +func NewGet(ctx *middleware.Context, handler GetHandler) *Get { + return &Get{Context: ctx, Handler: handler} +} + +/* + Get swagger:route GET /ipset/{name} ipset get + +Get a specific ipset and its members. +*/ +type Get struct { + Context *middleware.Context + Handler GetHandler +} + +func (o *Get) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewGetParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/get_all.go b/tools/dpvs-agent/restapi/operations/ipset/get_all.go new file mode 100644 index 000000000..d5a981a4a --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/get_all.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// GetAllHandlerFunc turns a function with the right signature into a get all handler +type GetAllHandlerFunc func(GetAllParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn GetAllHandlerFunc) Handle(params GetAllParams) middleware.Responder { + return fn(params) +} + +// GetAllHandler interface for that can handle valid get all params +type GetAllHandler interface { + Handle(GetAllParams) middleware.Responder +} + +// NewGetAll creates a new http.Handler for the get all operation +func NewGetAll(ctx *middleware.Context, handler GetAllHandler) *GetAll { + return &GetAll{Context: ctx, Handler: handler} +} + +/* + GetAll swagger:route GET /ipset ipset getAll + +Get all the ipsets and members. +*/ +type GetAll struct { + Context *middleware.Context + Handler GetAllHandler +} + +func (o *GetAll) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewGetAllParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/get_all_parameters.go b/tools/dpvs-agent/restapi/operations/ipset/get_all_parameters.go new file mode 100644 index 000000000..a92ff5454 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/get_all_parameters.go @@ -0,0 +1,46 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" +) + +// NewGetAllParams creates a new GetAllParams object +// +// There are no default values defined in the spec. +func NewGetAllParams() GetAllParams { + + return GetAllParams{} +} + +// GetAllParams contains all the bound params for the get all operation +// typically these are obtained from a http.Request +// +// swagger:parameters GetAll +type GetAllParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewGetAllParams() beforehand. +func (o *GetAllParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/get_all_responses.go b/tools/dpvs-agent/restapi/operations/ipset/get_all_responses.go new file mode 100644 index 000000000..433af030f --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/get_all_responses.go @@ -0,0 +1,102 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/dpvs-agent/models" +) + +// GetAllOKCode is the HTTP code returned for type GetAllOK +const GetAllOKCode int = 200 + +/* +GetAllOK Success + +swagger:response getAllOK +*/ +type GetAllOK struct { + + /* + In: Body + */ + Payload *models.IpsetInfoArray `json:"body,omitempty"` +} + +// NewGetAllOK creates GetAllOK with default headers values +func NewGetAllOK() *GetAllOK { + + return &GetAllOK{} +} + +// WithPayload adds the payload to the get all o k response +func (o *GetAllOK) WithPayload(payload *models.IpsetInfoArray) *GetAllOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get all o k response +func (o *GetAllOK) SetPayload(payload *models.IpsetInfoArray) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetAllOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// GetAllFailureCode is the HTTP code returned for type GetAllFailure +const GetAllFailureCode int = 500 + +/* +GetAllFailure Service not available + +swagger:response getAllFailure +*/ +type GetAllFailure struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewGetAllFailure creates GetAllFailure with default headers values +func NewGetAllFailure() *GetAllFailure { + + return &GetAllFailure{} +} + +// WithPayload adds the payload to the get all failure response +func (o *GetAllFailure) WithPayload(payload string) *GetAllFailure { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get all failure response +func (o *GetAllFailure) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetAllFailure) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/get_all_urlbuilder.go b/tools/dpvs-agent/restapi/operations/ipset/get_all_urlbuilder.go new file mode 100644 index 000000000..2fd0ac230 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/get_all_urlbuilder.go @@ -0,0 +1,87 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// GetAllURL generates an URL for the get all operation +type GetAllURL struct { + _basePath string +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetAllURL) WithBasePath(bp string) *GetAllURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetAllURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *GetAllURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/ipset" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/v2" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *GetAllURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *GetAllURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *GetAllURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on GetAllURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on GetAllURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *GetAllURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/get_parameters.go b/tools/dpvs-agent/restapi/operations/ipset/get_parameters.go new file mode 100644 index 000000000..10a335867 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/get_parameters.go @@ -0,0 +1,71 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" +) + +// NewGetParams creates a new GetParams object +// +// There are no default values defined in the spec. +func NewGetParams() GetParams { + + return GetParams{} +} + +// GetParams contains all the bound params for the get operation +// typically these are obtained from a http.Request +// +// swagger:parameters Get +type GetParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: path + */ + Name string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewGetParams() beforehand. +func (o *GetParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + rName, rhkName, _ := route.Params.GetOK("name") + if err := o.bindName(rName, rhkName, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindName binds and validates parameter Name from path. +func (o *GetParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Name = raw + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/get_responses.go b/tools/dpvs-agent/restapi/operations/ipset/get_responses.go new file mode 100644 index 000000000..c2018050a --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/get_responses.go @@ -0,0 +1,145 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/dpvs-agent/models" +) + +// GetOKCode is the HTTP code returned for type GetOK +const GetOKCode int = 200 + +/* +GetOK Success + +swagger:response getOK +*/ +type GetOK struct { + + /* + In: Body + */ + Payload *models.IpsetInfo `json:"body,omitempty"` +} + +// NewGetOK creates GetOK with default headers values +func NewGetOK() *GetOK { + + return &GetOK{} +} + +// WithPayload adds the payload to the get o k response +func (o *GetOK) WithPayload(payload *models.IpsetInfo) *GetOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get o k response +func (o *GetOK) SetPayload(payload *models.IpsetInfo) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// GetNotFoundCode is the HTTP code returned for type GetNotFound +const GetNotFoundCode int = 404 + +/* +GetNotFound Ipset not found + +swagger:response getNotFound +*/ +type GetNotFound struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewGetNotFound creates GetNotFound with default headers values +func NewGetNotFound() *GetNotFound { + + return &GetNotFound{} +} + +// WithPayload adds the payload to the get not found response +func (o *GetNotFound) WithPayload(payload string) *GetNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get not found response +func (o *GetNotFound) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// GetFailureCode is the HTTP code returned for type GetFailure +const GetFailureCode int = 500 + +/* +GetFailure Service not available + +swagger:response getFailure +*/ +type GetFailure struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewGetFailure creates GetFailure with default headers values +func NewGetFailure() *GetFailure { + + return &GetFailure{} +} + +// WithPayload adds the payload to the get failure response +func (o *GetFailure) WithPayload(payload string) *GetFailure { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get failure response +func (o *GetFailure) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetFailure) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/get_urlbuilder.go b/tools/dpvs-agent/restapi/operations/ipset/get_urlbuilder.go new file mode 100644 index 000000000..0eeff8b86 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/get_urlbuilder.go @@ -0,0 +1,99 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// GetURL generates an URL for the get operation +type GetURL struct { + Name string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetURL) WithBasePath(bp string) *GetURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *GetURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/ipset/{name}" + + name := o.Name + if name != "" { + _path = strings.Replace(_path, "{name}", name, -1) + } else { + return nil, errors.New("name is required on GetURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/v2" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *GetURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *GetURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *GetURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on GetURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on GetURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *GetURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/is_in.go b/tools/dpvs-agent/restapi/operations/ipset/is_in.go new file mode 100644 index 000000000..ef011b31a --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/is_in.go @@ -0,0 +1,120 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "context" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// IsInHandlerFunc turns a function with the right signature into a is in handler +type IsInHandlerFunc func(IsInParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn IsInHandlerFunc) Handle(params IsInParams) middleware.Responder { + return fn(params) +} + +// IsInHandler interface for that can handle valid is in params +type IsInHandler interface { + Handle(IsInParams) middleware.Responder +} + +// NewIsIn creates a new http.Handler for the is in operation +func NewIsIn(ctx *middleware.Context, handler IsInHandler) *IsIn { + return &IsIn{Context: ctx, Handler: handler} +} + +/* + IsIn swagger:route POST /ipset/{name}/cell ipset isIn + +Check if an object in the ipset. +*/ +type IsIn struct { + Context *middleware.Context + Handler IsInHandler +} + +func (o *IsIn) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewIsInParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} + +// IsInOKBody is in o k body +// +// swagger:model IsInOKBody +type IsInOKBody struct { + + // message + Message string `json:"Message,omitempty"` + + // result + // Required: true + Result *bool `json:"Result"` +} + +// Validate validates this is in o k body +func (o *IsInOKBody) Validate(formats strfmt.Registry) error { + var res []error + + if err := o.validateResult(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (o *IsInOKBody) validateResult(formats strfmt.Registry) error { + + if err := validate.Required("isInOK"+"."+"Result", "body", o.Result); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this is in o k body based on context it is used +func (o *IsInOKBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (o *IsInOKBody) MarshalBinary() ([]byte, error) { + if o == nil { + return nil, nil + } + return swag.WriteJSON(o) +} + +// UnmarshalBinary interface implementation +func (o *IsInOKBody) UnmarshalBinary(b []byte) error { + var res IsInOKBody + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *o = res + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/is_in_parameters.go b/tools/dpvs-agent/restapi/operations/ipset/is_in_parameters.go new file mode 100644 index 000000000..06236abce --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/is_in_parameters.go @@ -0,0 +1,101 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + + "github.com/dpvs-agent/models" +) + +// NewIsInParams creates a new IsInParams object +// +// There are no default values defined in the spec. +func NewIsInParams() IsInParams { + + return IsInParams{} +} + +// IsInParams contains all the bound params for the is in operation +// typically these are obtained from a http.Request +// +// swagger:parameters IsIn +type IsInParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + In: body + */ + IpsetCell *models.IpsetCell + /* + Required: true + In: path + */ + Name string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewIsInParams() beforehand. +func (o *IsInParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.IpsetCell + if err := route.Consumer.Consume(r.Body, &body); err != nil { + res = append(res, errors.NewParseError("ipsetCell", "body", "", err)) + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.IpsetCell = &body + } + } + } + + rName, rhkName, _ := route.Params.GetOK("name") + if err := o.bindName(rName, rhkName, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindName binds and validates parameter Name from path. +func (o *IsInParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Name = raw + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/is_in_responses.go b/tools/dpvs-agent/restapi/operations/ipset/is_in_responses.go new file mode 100644 index 000000000..4565ceabb --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/is_in_responses.go @@ -0,0 +1,186 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" +) + +// IsInOKCode is the HTTP code returned for type IsInOK +const IsInOKCode int = 200 + +/* +IsInOK Succeed + +swagger:response isInOK +*/ +type IsInOK struct { + + /* + In: Body + */ + Payload *IsInOKBody `json:"body,omitempty"` +} + +// NewIsInOK creates IsInOK with default headers values +func NewIsInOK() *IsInOK { + + return &IsInOK{} +} + +// WithPayload adds the payload to the is in o k response +func (o *IsInOK) WithPayload(payload *IsInOKBody) *IsInOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the is in o k response +func (o *IsInOK) SetPayload(payload *IsInOKBody) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *IsInOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// IsInBadRequestCode is the HTTP code returned for type IsInBadRequest +const IsInBadRequestCode int = 400 + +/* +IsInBadRequest Invalid ipset parameter + +swagger:response isInBadRequest +*/ +type IsInBadRequest struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewIsInBadRequest creates IsInBadRequest with default headers values +func NewIsInBadRequest() *IsInBadRequest { + + return &IsInBadRequest{} +} + +// WithPayload adds the payload to the is in bad request response +func (o *IsInBadRequest) WithPayload(payload string) *IsInBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the is in bad request response +func (o *IsInBadRequest) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *IsInBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// IsInNotFoundCode is the HTTP code returned for type IsInNotFound +const IsInNotFoundCode int = 404 + +/* +IsInNotFound Ipset not found + +swagger:response isInNotFound +*/ +type IsInNotFound struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewIsInNotFound creates IsInNotFound with default headers values +func NewIsInNotFound() *IsInNotFound { + + return &IsInNotFound{} +} + +// WithPayload adds the payload to the is in not found response +func (o *IsInNotFound) WithPayload(payload string) *IsInNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the is in not found response +func (o *IsInNotFound) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *IsInNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// IsInFailureCode is the HTTP code returned for type IsInFailure +const IsInFailureCode int = 500 + +/* +IsInFailure Service not available + +swagger:response isInFailure +*/ +type IsInFailure struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewIsInFailure creates IsInFailure with default headers values +func NewIsInFailure() *IsInFailure { + + return &IsInFailure{} +} + +// WithPayload adds the payload to the is in failure response +func (o *IsInFailure) WithPayload(payload string) *IsInFailure { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the is in failure response +func (o *IsInFailure) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *IsInFailure) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/is_in_urlbuilder.go b/tools/dpvs-agent/restapi/operations/ipset/is_in_urlbuilder.go new file mode 100644 index 000000000..4565d4dda --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/is_in_urlbuilder.go @@ -0,0 +1,99 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// IsInURL generates an URL for the is in operation +type IsInURL struct { + Name string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *IsInURL) WithBasePath(bp string) *IsInURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *IsInURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *IsInURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/ipset/{name}/cell" + + name := o.Name + if name != "" { + _path = strings.Replace(_path, "{name}", name, -1) + } else { + return nil, errors.New("name is required on IsInURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/v2" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *IsInURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *IsInURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *IsInURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on IsInURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on IsInURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *IsInURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/replace_member.go b/tools/dpvs-agent/restapi/operations/ipset/replace_member.go new file mode 100644 index 000000000..09668e599 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/replace_member.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// ReplaceMemberHandlerFunc turns a function with the right signature into a replace member handler +type ReplaceMemberHandlerFunc func(ReplaceMemberParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn ReplaceMemberHandlerFunc) Handle(params ReplaceMemberParams) middleware.Responder { + return fn(params) +} + +// ReplaceMemberHandler interface for that can handle valid replace member params +type ReplaceMemberHandler interface { + Handle(ReplaceMemberParams) middleware.Responder +} + +// NewReplaceMember creates a new http.Handler for the replace member operation +func NewReplaceMember(ctx *middleware.Context, handler ReplaceMemberHandler) *ReplaceMember { + return &ReplaceMember{Context: ctx, Handler: handler} +} + +/* + ReplaceMember swagger:route PUT /ipset/{name}/member ipset replaceMember + +Reset the whole ipset members. +*/ +type ReplaceMember struct { + Context *middleware.Context + Handler ReplaceMemberHandler +} + +func (o *ReplaceMember) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewReplaceMemberParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/replace_member_parameters.go b/tools/dpvs-agent/restapi/operations/ipset/replace_member_parameters.go new file mode 100644 index 000000000..95c1cb0da --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/replace_member_parameters.go @@ -0,0 +1,101 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + + "github.com/dpvs-agent/models" +) + +// NewReplaceMemberParams creates a new ReplaceMemberParams object +// +// There are no default values defined in the spec. +func NewReplaceMemberParams() ReplaceMemberParams { + + return ReplaceMemberParams{} +} + +// ReplaceMemberParams contains all the bound params for the replace member operation +// typically these are obtained from a http.Request +// +// swagger:parameters ReplaceMember +type ReplaceMemberParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + In: body + */ + IpsetParam *models.IpsetInfo + /* + Required: true + In: path + */ + Name string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewReplaceMemberParams() beforehand. +func (o *ReplaceMemberParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.IpsetInfo + if err := route.Consumer.Consume(r.Body, &body); err != nil { + res = append(res, errors.NewParseError("ipsetParam", "body", "", err)) + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.IpsetParam = &body + } + } + } + + rName, rhkName, _ := route.Params.GetOK("name") + if err := o.bindName(rName, rhkName, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindName binds and validates parameter Name from path. +func (o *ReplaceMemberParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Name = raw + + return nil +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/replace_member_responses.go b/tools/dpvs-agent/restapi/operations/ipset/replace_member_responses.go new file mode 100644 index 000000000..f402f2f43 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/replace_member_responses.go @@ -0,0 +1,184 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" +) + +// ReplaceMemberOKCode is the HTTP code returned for type ReplaceMemberOK +const ReplaceMemberOKCode int = 200 + +/* +ReplaceMemberOK Succeed + +swagger:response replaceMemberOK +*/ +type ReplaceMemberOK struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewReplaceMemberOK creates ReplaceMemberOK with default headers values +func NewReplaceMemberOK() *ReplaceMemberOK { + + return &ReplaceMemberOK{} +} + +// WithPayload adds the payload to the replace member o k response +func (o *ReplaceMemberOK) WithPayload(payload string) *ReplaceMemberOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the replace member o k response +func (o *ReplaceMemberOK) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ReplaceMemberOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// ReplaceMemberBadRequestCode is the HTTP code returned for type ReplaceMemberBadRequest +const ReplaceMemberBadRequestCode int = 400 + +/* +ReplaceMemberBadRequest Invalid ipset parameter + +swagger:response replaceMemberBadRequest +*/ +type ReplaceMemberBadRequest struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewReplaceMemberBadRequest creates ReplaceMemberBadRequest with default headers values +func NewReplaceMemberBadRequest() *ReplaceMemberBadRequest { + + return &ReplaceMemberBadRequest{} +} + +// WithPayload adds the payload to the replace member bad request response +func (o *ReplaceMemberBadRequest) WithPayload(payload string) *ReplaceMemberBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the replace member bad request response +func (o *ReplaceMemberBadRequest) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ReplaceMemberBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// ReplaceMemberNotFoundCode is the HTTP code returned for type ReplaceMemberNotFound +const ReplaceMemberNotFoundCode int = 404 + +/* +ReplaceMemberNotFound Ipset not found + +swagger:response replaceMemberNotFound +*/ +type ReplaceMemberNotFound struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewReplaceMemberNotFound creates ReplaceMemberNotFound with default headers values +func NewReplaceMemberNotFound() *ReplaceMemberNotFound { + + return &ReplaceMemberNotFound{} +} + +// WithPayload adds the payload to the replace member not found response +func (o *ReplaceMemberNotFound) WithPayload(payload string) *ReplaceMemberNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the replace member not found response +func (o *ReplaceMemberNotFound) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ReplaceMemberNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// ReplaceMemberFailureCode is the HTTP code returned for type ReplaceMemberFailure +const ReplaceMemberFailureCode int = 500 + +/* +ReplaceMemberFailure Service not available + +swagger:response replaceMemberFailure +*/ +type ReplaceMemberFailure struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewReplaceMemberFailure creates ReplaceMemberFailure with default headers values +func NewReplaceMemberFailure() *ReplaceMemberFailure { + + return &ReplaceMemberFailure{} +} + +// WithPayload adds the payload to the replace member failure response +func (o *ReplaceMemberFailure) WithPayload(payload string) *ReplaceMemberFailure { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the replace member failure response +func (o *ReplaceMemberFailure) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ReplaceMemberFailure) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/tools/dpvs-agent/restapi/operations/ipset/replace_member_urlbuilder.go b/tools/dpvs-agent/restapi/operations/ipset/replace_member_urlbuilder.go new file mode 100644 index 000000000..e49b05b76 --- /dev/null +++ b/tools/dpvs-agent/restapi/operations/ipset/replace_member_urlbuilder.go @@ -0,0 +1,99 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ipset + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// ReplaceMemberURL generates an URL for the replace member operation +type ReplaceMemberURL struct { + Name string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *ReplaceMemberURL) WithBasePath(bp string) *ReplaceMemberURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *ReplaceMemberURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *ReplaceMemberURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/ipset/{name}/member" + + name := o.Name + if name != "" { + _path = strings.Replace(_path, "{name}", name, -1) + } else { + return nil, errors.New("name is required on ReplaceMemberURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/v2" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *ReplaceMemberURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *ReplaceMemberURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *ReplaceMemberURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on ReplaceMemberURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on ReplaceMemberURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *ReplaceMemberURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} From 86a5ea521d95fa354df2e8171349dc8f277bfc1c Mon Sep 17 00:00:00 2001 From: ywc689 Date: Thu, 15 Aug 2024 11:35:07 +0800 Subject: [PATCH 53/63] dpvs-agent: implementation of ipset api Signed-off-by: ywc689 --- include/conf/ipset.h | 57 +- include/ipset/ipset.h | 2 +- src/ipset/ipset_bitmap.c | 13 +- src/ipset/ipset_hash.c | 8 +- tools/dpip/ipset.c | 2 +- .../cmd/dpvs-agent-server/api_init.go | 23 + tools/dpvs-agent/cmd/ipset/add_member.go | 79 + tools/dpvs-agent/cmd/ipset/create.go | 75 + tools/dpvs-agent/cmd/ipset/del_member.go | 77 + tools/dpvs-agent/cmd/ipset/destroy.go | 56 + tools/dpvs-agent/cmd/ipset/get.go | 63 + tools/dpvs-agent/cmd/ipset/get_all.go | 54 + tools/dpvs-agent/cmd/ipset/is_in.go | 81 + tools/dpvs-agent/cmd/ipset/replace_member.go | 81 + tools/dpvs-agent/pkg/ipc/types/certificate.go | 3 - tools/dpvs-agent/pkg/ipc/types/const.go | 5 + tools/dpvs-agent/pkg/ipc/types/ipset.go | 948 ++++++++++++ .../dpvs-agent/pkg/ipc/types/ipset_models.go | 1305 +++++++++++++++++ 18 files changed, 2898 insertions(+), 34 deletions(-) create mode 100644 tools/dpvs-agent/cmd/ipset/add_member.go create mode 100644 tools/dpvs-agent/cmd/ipset/create.go create mode 100644 tools/dpvs-agent/cmd/ipset/del_member.go create mode 100644 tools/dpvs-agent/cmd/ipset/destroy.go create mode 100644 tools/dpvs-agent/cmd/ipset/get.go create mode 100644 tools/dpvs-agent/cmd/ipset/get_all.go create mode 100644 tools/dpvs-agent/cmd/ipset/is_in.go create mode 100644 tools/dpvs-agent/cmd/ipset/replace_member.go create mode 100644 tools/dpvs-agent/pkg/ipc/types/ipset.go create mode 100644 tools/dpvs-agent/pkg/ipc/types/ipset_models.go diff --git a/include/conf/ipset.h b/include/conf/ipset.h index 7a080a226..1e59cc66c 100644 --- a/include/conf/ipset.h +++ b/include/conf/ipset.h @@ -32,7 +32,7 @@ #define IPSET_F_FORCE 0x0001 enum ipset_op { - IPSET_OP_ADD, + IPSET_OP_ADD = 1, IPSET_OP_DEL, IPSET_OP_TEST, IPSET_OP_CREATE, @@ -43,34 +43,36 @@ enum ipset_op { }; struct ipset_option { - int family; union { struct { - bool comment; - int hashsize; - int maxelem; - } create; + int32_t hashsize; + uint32_t maxelem; + uint8_t comment; + } __attribute__((__packed__)) create; struct { - bool nomatch; - } add; + char padding[8]; + uint8_t nomatch; + } __attribute__((__packed__)) add; }; -}; + uint8_t family; +} __attribute__((__packed__)); struct ipset_param { char type[IPSET_MAXNAMELEN]; char name[IPSET_MAXNAMELEN]; char comment[IPSET_MAXCOMLEN]; - int opcode; - struct ipset_option option; + uint16_t opcode; uint16_t flag; + struct ipset_option option; uint8_t proto; uint8_t cidr; struct inet_addr_range range; /* port in host byteorder */ - uint8_t mac[6]; char iface[IFNAMSIZ]; + uint8_t mac[6]; /* for type with 2 nets */ + uint8_t padding; uint8_t cidr2; struct inet_addr_range range2; //uint8_t mac[2]; @@ -83,43 +85,48 @@ struct ipset_member { uint8_t cidr; uint8_t proto; uint16_t port; - uint8_t mac[6]; char iface[IFNAMSIZ]; - bool nomatch; + uint8_t mac[6]; + uint8_t nomatch; /* second net */ - union inet_addr addr2; uint8_t cidr2; uint16_t port2; + uint8_t padding[2]; + union inet_addr addr2; }; struct ipset_info { char name[IPSET_MAXNAMELEN]; char type[IPSET_MAXNAMELEN]; - bool comment; + uint8_t comment; + + uint8_t af; + uint8_t padding[2]; union { struct ipset_bitmap_header { - struct inet_addr_range range; uint8_t cidr; + uint8_t padding[3]; + struct inet_addr_range range; } bitmap; struct ipset_hash_header { - int hashsize; - int maxelem; + uint8_t padding[4]; // aligned for dpvs-agent + int32_t hashsize; + uint32_t maxelem; } hash; }; - int af; - size_t size; - int entries; - int references; + uint32_t size; + uint32_t entries; + uint32_t references; void *members; }; struct ipset_info_array { - int nipset; - struct ipset_info infos[0]; + uint32_t nipset; + struct ipset_info infos[0]; } __attribute__((__packed__)); #endif /* __DPVS_IPSET_CONF_H__ */ diff --git a/include/ipset/ipset.h b/include/ipset/ipset.h index 68ded707a..6c1d3649a 100644 --- a/include/ipset/ipset.h +++ b/include/ipset/ipset.h @@ -30,7 +30,7 @@ #define IPSET #define RTE_LOGTYPE_IPSET RTE_LOGTYPE_USER1 -#define IPSET_ADT_MAX 3 +#define IPSET_ADT_MAX IPSET_OP_MAX struct ipset; diff --git a/src/ipset/ipset_bitmap.c b/src/ipset/ipset_bitmap.c index eaa6d376e..63a554635 100644 --- a/src/ipset/ipset_bitmap.c +++ b/src/ipset/ipset_bitmap.c @@ -32,7 +32,7 @@ bitmap_add(struct ipset *set, void *value, uint16_t flag) /* To avoid same IP, different MAC or other elements */ if (ret || test_bit(e->id, map->members)) { - if (flag & IPSET_F_FORCE) + if (flag & IPSET_F_FORCE) return EDPVS_OK; return EDPVS_EXIST; } @@ -51,8 +51,11 @@ bitmap_del(struct ipset *set, void *value, uint16_t flag) if (e->id >= map->elements) return EDPVS_INVAL; - if (!do(del, value, map)) + if (!do(del, value, map)) { + if (flag & IPSET_F_FORCE) + return EDPVS_OK; return EDPVS_NOTEXIST; + } set->elements--; return EDPVS_OK; @@ -70,7 +73,11 @@ bitmap_test(struct ipset *set, void *value, uint16_t flag) return do(test, value, map, set->dsize); } -ipset_adtfn bitmap_adtfn[IPSET_ADT_MAX] = { bitmap_add, bitmap_del, bitmap_test }; +ipset_adtfn bitmap_adtfn[IPSET_ADT_MAX] = { + [ IPSET_OP_ADD ] = bitmap_add, + [ IPSET_OP_DEL ] = bitmap_del, + [ IPSET_OP_TEST ] = bitmap_test +}; void bitmap_flush(struct ipset *set) diff --git a/src/ipset/ipset_hash.c b/src/ipset/ipset_hash.c index e4b93e18f..07c35eaad 100644 --- a/src/ipset/ipset_hash.c +++ b/src/ipset/ipset_hash.c @@ -160,6 +160,8 @@ hash_del(struct ipset *set, void *value, uint16_t flag) return EDPVS_OK; } } + if (flag & IPSET_F_FORCE) + return EDPVS_OK; return EDPVS_NOTEXIST; } @@ -245,7 +247,11 @@ hash_test(struct ipset *set, void *value, uint16_t flag) return 0; } -ipset_adtfn hash_adtfn[IPSET_ADT_MAX] = { hash_add, hash_del, hash_test }; +ipset_adtfn hash_adtfn[IPSET_ADT_MAX] = { + [ IPSET_OP_ADD ] = hash_add, + [ IPSET_OP_DEL ] = hash_del, + [ IPSET_OP_TEST ] = hash_test +}; void hash_flush(struct ipset *set) diff --git a/tools/dpip/ipset.c b/tools/dpip/ipset.c index 8f324d2e1..892b2374f 100644 --- a/tools/dpip/ipset.c +++ b/tools/dpip/ipset.c @@ -147,7 +147,7 @@ static int addr_arg_parse(char *arg, struct inet_addr_range *range, uint8_t *cidr) { char *ip1, *ip2, *sep; - int *af = ¶m.option.family; + uint8_t *af = ¶m.option.family; /* ip/cidr */ if (cidr && (sep = strstr(arg, "/"))) { diff --git a/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go b/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go index 80b00831e..9fbb13937 100644 --- a/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go +++ b/tools/dpvs-agent/cmd/dpvs-agent-server/api_init.go @@ -28,6 +28,7 @@ import ( rotatelogs "github.com/lestrrat-go/file-rotatelogs" "github.com/dpvs-agent/cmd/device" + "github.com/dpvs-agent/cmd/ipset" "github.com/dpvs-agent/cmd/ipvs" "github.com/dpvs-agent/pkg/ipc/pool" "github.com/dpvs-agent/pkg/settings" @@ -187,6 +188,8 @@ func (agent *DpvsAgentServer) instantiateAPI(restAPI *operations.DpvsAgentAPI) { logger := hclog.Default().Named("main") + //////////////////////////////////// ipvs /////////////////////////////////////////// + // delete restAPI.VirtualserverDeleteVsVipPortHandler = ipvs.NewDelVsItem(cp, logger) restAPI.VirtualserverDeleteVsVipPortLaddrHandler = ipvs.NewDelVsLaddr(cp, logger) @@ -210,6 +213,8 @@ func (agent *DpvsAgentServer) instantiateAPI(restAPI *operations.DpvsAgentAPI) { // post restAPI.VirtualserverPostVsVipPortRsHandler = ipvs.NewPostVsRs(cp, logger) + //////////////////////////////////// device /////////////////////////////////////////// + // get // restAPI.DeviceGetDeviceNameAddrHandler // restAPI.DeviceGetDeviceNameRouteHandler @@ -230,6 +235,24 @@ func (agent *DpvsAgentServer) instantiateAPI(restAPI *operations.DpvsAgentAPI) { restAPI.DeviceDeleteDeviceNameVlanHandler = device.NewDelDeviceVlan(cp, logger) restAPI.DeviceDeleteDeviceNameNetlinkAddrHandler = device.NewDelDeviceNetlinkAddr(cp, logger) + //////////////////////////////////// ipset /////////////////////////////////////////// + + // GET + restAPI.IpsetGetHandler = ipset.NewIpsetGet(cp, logger) + restAPI.IpsetGetAllHandler = ipset.NewIpsetGetAll(cp, logger) + + // POST + restAPI.IpsetIsInHandler = ipset.NewIpsetIsIn(cp, logger) + restAPI.IpsetAddMemberHandler = ipset.NewIpsetAddMember(cp, logger) + + // PUT + restAPI.IpsetCreateHandler = ipset.NewIpsetCreate(cp, logger) + restAPI.IpsetReplaceMemberHandler = ipset.NewIpsetReplaceMember(cp, logger) + + // DELETE + restAPI.IpsetDestroyHandler = ipset.NewIpsetDestroy(cp, logger) + restAPI.IpsetDelMemberHandler = ipset.NewIpsetDelMember(cp, logger) + switch strings.ToLower(agent.InitMode) { case "network": case "local": diff --git a/tools/dpvs-agent/cmd/ipset/add_member.go b/tools/dpvs-agent/cmd/ipset/add_member.go new file mode 100644 index 000000000..71efaca51 --- /dev/null +++ b/tools/dpvs-agent/cmd/ipset/add_member.go @@ -0,0 +1,79 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipset + +import ( + "fmt" + + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/ipc/types" + api "github.com/dpvs-agent/restapi/operations/ipset" + "github.com/go-openapi/runtime/middleware" + "github.com/hashicorp/go-hclog" +) + +type ipsetAddMember struct { + connPool *pool.ConnPool + logger hclog.Logger +} + +func NewIpsetAddMember(cp *pool.ConnPool, parentLogger hclog.Logger) *ipsetAddMember { + logger := hclog.Default() + if parentLogger != nil { + logger = parentLogger.Named("ipsetAddMember") + } + return &ipsetAddMember{connPool: cp, logger: logger} +} + +func (h *ipsetAddMember) Handle(params api.AddMemberParams) middleware.Responder { + if params.IpsetParam == nil { + return api.NewAddMemberBadRequest().WithPayload("missing ipset param") + } + + if *params.IpsetParam.Name != params.Name { + return api.NewAddMemberBadRequest().WithPayload("ipset name mismatch") + } + + if params.IpsetParam.CreationOptions != nil { + return api.NewAddMemberBadRequest().WithPayload("CreationOptions set in adding member") + } + + conf := types.IPSetParamArray{} + if err := conf.Build(types.IPSET_OP_ADD, params.IpsetParam); err != nil { + return api.NewAddMemberBadRequest().WithPayload(fmt.Sprintf( + "build AddMember param failed: %s", err.Error())) + } + + if err := conf.Check(); err != nil { + return api.NewAddMemberBadRequest().WithPayload(fmt.Sprintf( + "AddMember params check failed: %s", err.Error())) + } + + err, derr := conf.AddDelMember(h.connPool, h.logger) + if derr == types.EDPVS_EXIST { + return api.NewAddMemberOK().WithPayload(fmt.Sprintf("%s (may partially succeed)", derr.String())) + } + if err != nil { + h.logger.Error("Ipset AddMember failed.", "setName", params.Name, "Reason", err.Error()) + if derr == types.EDPVS_NOTEXIST { + return api.NewAddMemberNotFound().WithPayload(derr.String()) + } + return api.NewAddMemberFailure().WithPayload(err.Error()) + } + + h.logger.Info("Ipset AddMember succeed.", "setName", params.Name) + return api.NewAddMemberCreated().WithPayload(fmt.Sprintf("ipset %s add members succeed", + params.Name)) +} diff --git a/tools/dpvs-agent/cmd/ipset/create.go b/tools/dpvs-agent/cmd/ipset/create.go new file mode 100644 index 000000000..5d5925872 --- /dev/null +++ b/tools/dpvs-agent/cmd/ipset/create.go @@ -0,0 +1,75 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipset + +import ( + "fmt" + + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/ipc/types" + api "github.com/dpvs-agent/restapi/operations/ipset" + "github.com/go-openapi/runtime/middleware" + "github.com/hashicorp/go-hclog" +) + +type ipsetCreate struct { + connPool *pool.ConnPool + logger hclog.Logger +} + +func NewIpsetCreate(cp *pool.ConnPool, parentLogger hclog.Logger) *ipsetCreate { + logger := hclog.Default() + if parentLogger != nil { + logger = parentLogger.Named("ipsetCreate") + } + return &ipsetCreate{connPool: cp, logger: logger} +} + +func (h *ipsetCreate) Handle(params api.CreateParams) middleware.Responder { + if params.IpsetParam == nil { + return api.NewCreateBadRequest().WithPayload("missing ipset param") + } + + if *params.IpsetParam.Name != params.Name { + return api.NewCreateBadRequest().WithPayload("ipset name mismatch") + } + + conf := types.IPSetParam{} + conf.SetOpcode(types.IPSET_OP_CREATE) + if err := conf.Build(params.IpsetParam); err != nil { + return api.NewCreateBadRequest().WithPayload(fmt.Sprintf( + "build create param failed: %s", err.Error())) + } + + if err := conf.Check(); err != nil { + return api.NewCreateBadRequest().WithPayload(fmt.Sprintf("invalid create params: %s", + err.Error())) + } + + err, derr := conf.CreateDestroy(h.connPool, h.logger) + if derr == types.EDPVS_EXIST { + return api.NewCreateOK().WithPayload(derr.String()) + } + if err != nil { + h.logger.Error("Ipset Create failed.", "setName", params.Name, "Reason", err.Error()) + if derr == types.EDPVS_NOTEXIST { + return api.NewCreateNotFound().WithPayload(derr.String()) + } + return api.NewCreateFailure().WithPayload(err.Error()) + } + + h.logger.Info("Ipset Create succeed.", "setName", params.Name) + return api.NewCreateCreated().WithPayload(fmt.Sprintf("ipset %s created", params.Name)) +} diff --git a/tools/dpvs-agent/cmd/ipset/del_member.go b/tools/dpvs-agent/cmd/ipset/del_member.go new file mode 100644 index 000000000..fa2a8532c --- /dev/null +++ b/tools/dpvs-agent/cmd/ipset/del_member.go @@ -0,0 +1,77 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipset + +import ( + "fmt" + + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/ipc/types" + api "github.com/dpvs-agent/restapi/operations/ipset" + "github.com/go-openapi/runtime/middleware" + "github.com/hashicorp/go-hclog" +) + +type ipsetDelMember struct { + connPool *pool.ConnPool + logger hclog.Logger +} + +func NewIpsetDelMember(cp *pool.ConnPool, parentLogger hclog.Logger) *ipsetDelMember { + logger := hclog.Default() + if parentLogger != nil { + logger = parentLogger.Named("ipsetDelMember") + } + return &ipsetDelMember{connPool: cp, logger: logger} +} + +func (h *ipsetDelMember) Handle(params api.DelMemberParams) middleware.Responder { + if params.IpsetParam == nil { + return api.NewDelMemberBadRequest().WithPayload("missing ipset param") + } + + if *params.IpsetParam.Name != params.Name { + return api.NewDelMemberBadRequest().WithPayload("ipset name mismatch") + } + + if params.IpsetParam.CreationOptions != nil { + return api.NewDelMemberBadRequest().WithPayload("CreationOptions set in deleting member") + } + + conf := types.IPSetParamArray{} + if err := conf.Build(types.IPSET_OP_DEL, params.IpsetParam); err != nil { + return api.NewDelMemberBadRequest().WithPayload(fmt.Sprintf( + "build DelMember param failed: %s", err.Error())) + } + + if err := conf.Check(); err != nil { + return api.NewDelMemberBadRequest().WithPayload(fmt.Sprintf( + "DelMember params check failed: %s", err.Error())) + } + + err, derr := conf.AddDelMember(h.connPool, h.logger) + if derr == types.EDPVS_NOTEXIST { + return api.NewDelMemberNotFound().WithPayload(fmt.Sprintf("%s(may partially deleted)", + derr.String())) + } + if err != nil { + h.logger.Error("Ipset DelMember failed.", "setName", params.Name, "Reason", err.Error()) + return api.NewDelMemberFailure().WithPayload(err.Error()) + } + + h.logger.Info("Ipset DelMember succeed.", "setName", params.Name) + return api.NewDelMemberOK().WithPayload(fmt.Sprintf("ipset %s delete members succeed", + params.Name)) +} diff --git a/tools/dpvs-agent/cmd/ipset/destroy.go b/tools/dpvs-agent/cmd/ipset/destroy.go new file mode 100644 index 000000000..b44e1d53e --- /dev/null +++ b/tools/dpvs-agent/cmd/ipset/destroy.go @@ -0,0 +1,56 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipset + +import ( + "fmt" + + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/ipc/types" + api "github.com/dpvs-agent/restapi/operations/ipset" + "github.com/go-openapi/runtime/middleware" + "github.com/hashicorp/go-hclog" +) + +type ipsetDestroy struct { + connPool *pool.ConnPool + logger hclog.Logger +} + +func NewIpsetDestroy(cp *pool.ConnPool, parentLogger hclog.Logger) *ipsetDestroy { + logger := hclog.Default() + if parentLogger != nil { + logger = parentLogger.Named("ipsetDestroy") + } + return &ipsetDestroy{connPool: cp, logger: logger} +} + +func (h *ipsetDestroy) Handle(params api.DestroyParams) middleware.Responder { + conf := types.IPSetParam{} + conf.SetOpcode(types.IPSET_OP_DESTROY) + conf.SetName(params.Name) + + err, derr := conf.CreateDestroy(h.connPool, h.logger) + if derr == types.EDPVS_NOTEXIST { + return api.NewDestroyNotFound().WithPayload(derr.String()) + } + if err != nil { + h.logger.Error("Ipset Destroy failed.", "setName", params.Name, "Reason", err.Error()) + return api.NewDestroyFailure().WithPayload(err.Error()) + } + + h.logger.Info("Ipset Destroy succeed.", "setName", params.Name) + return api.NewDestroyOK().WithPayload(fmt.Sprintf("ipset %s destroyed", params.Name)) +} diff --git a/tools/dpvs-agent/cmd/ipset/get.go b/tools/dpvs-agent/cmd/ipset/get.go new file mode 100644 index 000000000..998321e52 --- /dev/null +++ b/tools/dpvs-agent/cmd/ipset/get.go @@ -0,0 +1,63 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipset + +import ( + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/ipc/types" + api "github.com/dpvs-agent/restapi/operations/ipset" + "github.com/go-openapi/runtime/middleware" + "github.com/hashicorp/go-hclog" +) + +type ipsetGet struct { + connPool *pool.ConnPool + logger hclog.Logger +} + +func NewIpsetGet(cp *pool.ConnPool, parentLogger hclog.Logger) *ipsetGet { + logger := hclog.Default() + if parentLogger != nil { + logger = parentLogger.Named("ipsetGet") + } + return &ipsetGet{connPool: cp, logger: logger} +} + +func (h *ipsetGet) Handle(params api.GetParams) middleware.Responder { + conf := &types.IPSetParam{} + + conf.SetOpcode(types.IPSET_OP_LIST) + conf.SetName(params.Name) + infos, err, derr := conf.Get(h.connPool, h.logger) + if err != nil { + h.logger.Error("Ipset Get failed.", "setName", params.Name, "Reason", err.Error()) + if derr == types.EDPVS_NOTEXIST { + return api.NewGetNotFound().WithPayload(derr.String()) + } + return api.NewGetFailure().WithPayload(err.Error()) + } + + h.logger.Info("Ipset Get succeed", "setName", params.Name) + model, err := infos.Model() + if err != nil { + h.logger.Error("Modelling ipset Get result failed.", "setName", params.Name, "Reason", err.Error()) + } + + resp := api.NewGetOK() + if model.Count > 0 { + resp.SetPayload(model.Infos[0]) + } + return resp +} diff --git a/tools/dpvs-agent/cmd/ipset/get_all.go b/tools/dpvs-agent/cmd/ipset/get_all.go new file mode 100644 index 000000000..2e654e92e --- /dev/null +++ b/tools/dpvs-agent/cmd/ipset/get_all.go @@ -0,0 +1,54 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipset + +import ( + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/ipc/types" + api "github.com/dpvs-agent/restapi/operations/ipset" + "github.com/go-openapi/runtime/middleware" + "github.com/hashicorp/go-hclog" +) + +type ipsetGetAll struct { + connPool *pool.ConnPool + logger hclog.Logger +} + +func NewIpsetGetAll(cp *pool.ConnPool, parentLogger hclog.Logger) *ipsetGetAll { + logger := hclog.Default() + if parentLogger != nil { + logger = parentLogger.Named("ipsetGetAll") + } + return &ipsetGetAll{connPool: cp, logger: logger} +} + +func (h *ipsetGetAll) Handle(params api.GetAllParams) middleware.Responder { + conf := &types.IPSetParam{} + + conf.SetOpcode(types.IPSET_OP_LIST) + infos, err, _ := conf.Get(h.connPool, h.logger) + if err != nil { + h.logger.Error("Ipset GetAll failed.", "Reason", err.Error()) + return api.NewGetAllFailure().WithPayload(err.Error()) + } + + h.logger.Info("Ipset GetAll succeed") + model, err := infos.Model() + if err != nil { + h.logger.Error("Modelling ipset GetAll result failed.", "Reason", err.Error()) + } + return api.NewGetAllOK().WithPayload(model) +} diff --git a/tools/dpvs-agent/cmd/ipset/is_in.go b/tools/dpvs-agent/cmd/ipset/is_in.go new file mode 100644 index 000000000..eadd3f1f2 --- /dev/null +++ b/tools/dpvs-agent/cmd/ipset/is_in.go @@ -0,0 +1,81 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipset + +import ( + "fmt" + + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/ipc/types" + api "github.com/dpvs-agent/restapi/operations/ipset" + "github.com/go-openapi/runtime/middleware" + "github.com/hashicorp/go-hclog" +) + +type ipsetIsIn struct { + connPool *pool.ConnPool + logger hclog.Logger +} + +func NewIpsetIsIn(cp *pool.ConnPool, parentLogger hclog.Logger) *ipsetIsIn { + logger := hclog.Default() + if parentLogger != nil { + logger = parentLogger.Named("ipsetIsIn") + } + return &ipsetIsIn{connPool: cp, logger: logger} +} + +func (h *ipsetIsIn) Handle(params api.IsInParams) middleware.Responder { + if params.IpsetCell == nil { + return api.NewIsInBadRequest().WithPayload("missing ipset entry") + } + + conf := types.IPSetParam{} + conf.SetOpcode(types.IPSET_OP_TEST) + conf.SetName(params.Name) + conf.SetKind(string(*params.IpsetCell.Type)) + if err := conf.BuildMember(params.IpsetCell.Member); err != nil { + return api.NewIsInBadRequest().WithPayload(fmt.Sprintf("invalid member: %s", err.Error())) + } + + if err := conf.Check(); err != nil { + return api.NewIsInBadRequest().WithPayload(fmt.Sprintf("invalid param: %s", err.Error())) + } + + result, err, derr := conf.IsIn(h.connPool, h.logger) + if err != nil { + h.logger.Error("Ipset IsIn failed.", "setName", params.Name, "Reason", err.Error()) + if derr == types.EDPVS_NOTEXIST { + return api.NewIsInNotFound().WithPayload(derr.String()) + } + return api.NewIsInFailure().WithPayload(err.Error()) + } + h.logger.Info("Ipset InIn succeed.", "setName", params.Name) + + nomatch := "" + if params.IpsetCell.Member.Options != nil && + params.IpsetCell.Member.Options.NoMatch != nil && + *params.IpsetCell.Member.Options.NoMatch { + nomatch = " (nomatch)" + } + + msg := "" + if result { + msg = fmt.Sprintf("%s%s is IN set %s", nomatch, *params.IpsetCell.Member.Entry, params.Name) + } else { + msg = fmt.Sprintf("%s%s is NOT IN set %s", nomatch, *params.IpsetCell.Member.Entry, params.Name) + } + return api.NewIsInOK().WithPayload(&api.IsInOKBody{Result: &result, Message: msg}) +} diff --git a/tools/dpvs-agent/cmd/ipset/replace_member.go b/tools/dpvs-agent/cmd/ipset/replace_member.go new file mode 100644 index 000000000..73b6c5156 --- /dev/null +++ b/tools/dpvs-agent/cmd/ipset/replace_member.go @@ -0,0 +1,81 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipset + +import ( + "fmt" + + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/dpvs-agent/pkg/ipc/types" + api "github.com/dpvs-agent/restapi/operations/ipset" + "github.com/go-openapi/runtime/middleware" + "github.com/hashicorp/go-hclog" +) + +type ipsetReplaceMember struct { + connPool *pool.ConnPool + logger hclog.Logger +} + +func NewIpsetReplaceMember(cp *pool.ConnPool, parentLogger hclog.Logger) *ipsetReplaceMember { + logger := hclog.Default() + if parentLogger != nil { + logger = parentLogger.Named("ipsetReplaceMember") + } + return &ipsetReplaceMember{connPool: cp, logger: logger} +} + +func (h *ipsetReplaceMember) Handle(params api.ReplaceMemberParams) middleware.Responder { + if params.IpsetParam == nil { + return api.NewReplaceMemberBadRequest().WithPayload("missing ipset param") + } + + if *params.IpsetParam.Name != params.Name { + return api.NewReplaceMemberBadRequest().WithPayload("ipset name mismatch") + } + + if params.IpsetParam.CreationOptions != nil { + return api.NewReplaceMemberBadRequest().WithPayload("CreationOptions set in replacing member") + } + + opcode := types.IPSET_OP_FLUSH + if len(params.IpsetParam.Entries) > 0 { + opcode = types.IPSET_OP_ADD + } + + conf := types.IPSetParamArray{} + if err := conf.Build(opcode, params.IpsetParam); err != nil { + return api.NewReplaceMemberBadRequest().WithPayload(fmt.Sprintf( + "build ReplaceMember param failed: %s", err.Error())) + } + + if err := conf.Check(); err != nil { + return api.NewReplaceMemberBadRequest().WithPayload(fmt.Sprintf( + "ReplaceMember params check failed: %s", err.Error())) + } + + err, derr := conf.ReplaceMember(h.connPool, h.logger) + if derr == types.EDPVS_NOTEXIST { + return api.NewReplaceMemberNotFound().WithPayload(derr.String()) + } + if err != nil { + h.logger.Error("Ipset ReplaceMember failed.", "setName", params.Name, "Reason", err.Error()) + return api.NewReplaceMemberFailure().WithPayload(err.Error()) + } + + h.logger.Info("Ipset ReplaceMember succeed.", "setName", params.Name) + return api.NewReplaceMemberOK().WithPayload(fmt.Sprintf("ipset %s replace members succeed", + params.Name)) +} diff --git a/tools/dpvs-agent/pkg/ipc/types/certificate.go b/tools/dpvs-agent/pkg/ipc/types/certificate.go index 770a9bdeb..7916bd897 100644 --- a/tools/dpvs-agent/pkg/ipc/types/certificate.go +++ b/tools/dpvs-agent/pkg/ipc/types/certificate.go @@ -31,9 +31,6 @@ import ( "github.com/dpvs-agent/pkg/ipc/pool" ) -/* derived from: include/conf/ipset.h */ -const IPSET_MAXNAMELEN = 32 - /* derived from: - include/conf/blklst.h diff --git a/tools/dpvs-agent/pkg/ipc/types/const.go b/tools/dpvs-agent/pkg/ipc/types/const.go index 590b48d14..ade0c16c8 100644 --- a/tools/dpvs-agent/pkg/ipc/types/const.go +++ b/tools/dpvs-agent/pkg/ipc/types/const.go @@ -241,6 +241,7 @@ const ( SOCKOPT_SET_IFADDR_SET SOCKOPT_SET_IFADDR_FLUSH SOCKOPT_GET_IFADDR_SHOW + SOCKOPT_GET_IFMADDR_SHOW SOCKOPT_NETIF_SET_LCORE SOCKOPT_NETIF_SET_PORT @@ -255,8 +256,12 @@ const ( SOCKOPT_NETIF_GET_PORT_XSTATS SOCKOPT_NETIF_GET_PORT_EXT_INFO SOCKOPT_NETIF_GET_BOND_STATUS + SOCKOPT_NETIF_GET_MADDR SOCKOPT_NETIF_GET_MAX + SOCKOPT_SET_LLDP_TODO + SOCKOPT_GET_LLDP_SHOW + SOCKOPT_SET_NEIGH_ADD SOCKOPT_SET_NEIGH_DEL SOCKOPT_GET_NEIGH_SHOW diff --git a/tools/dpvs-agent/pkg/ipc/types/ipset.go b/tools/dpvs-agent/pkg/ipc/types/ipset.go new file mode 100644 index 000000000..eec5d508f --- /dev/null +++ b/tools/dpvs-agent/pkg/ipc/types/ipset.go @@ -0,0 +1,948 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "net" + "syscall" + "unsafe" + + "github.com/dpvs-agent/pkg/ipc/pool" + "github.com/hashicorp/go-hclog" + "golang.org/x/sys/unix" +) + +// The consts mirrors const macros defined in conf/ipset.h +const ( + IPSET_MAXNAMELEN = 32 + IPSET_MAXCOMLEN = 32 + + IPSET_F_FORCE = 0x0001 +) + +// The consts mirrors `enum ipset_op` defined in conf/ipset.h +const ( + _ uint16 = iota + IPSET_OP_ADD + IPSET_OP_DEL + IPSET_OP_TEST + IPSET_OP_CREATE + IPSET_OP_DESTROY + IPSET_OP_FLUSH + IPSET_OP_LIST + IPSET_OP_MAX +) + +// InetAddrRange mirrors `struct inet_addr_range` defined in conf/inet.h +type InetAddrRange struct { + minAddr [16]byte + maxAddr [16]byte + minPort uint16 + maxPort uint16 +} + +func (o *InetAddrRange) SetMinAddr(ip net.IP) { + if ip == nil { + return + } + if ip4 := ip.To4(); ip4 != nil { + copy(o.minAddr[:4], ip4[:4]) + } else { + copy(o.minAddr[:], ip[:]) + } +} + +func (o *InetAddrRange) SetMaxAddr(ip net.IP) { + if ip == nil { + return + } + if ip4 := ip.To4(); ip4 != nil { + copy(o.maxAddr[:4], ip4[:4]) + } else { + copy(o.maxAddr[:], ip[:]) + } +} + +func (o *InetAddrRange) SetMinPort(port uint16) { + o.minPort = port +} + +func (o *InetAddrRange) SetMaxPort(port uint16) { + o.maxPort = port +} + +func (o *InetAddrRange) Decode(af uint8) (net.IP, net.IP, uint16, uint16) { + if af == syscall.AF_INET6 { + minAddr := make(net.IP, net.IPv6len) + maxAddr := make(net.IP, net.IPv6len) + copy(minAddr[:], o.minAddr[:16]) + copy(maxAddr[:], o.maxAddr[:16]) + return minAddr, maxAddr, o.minPort, o.maxPort + } else { + minAddr := net.IPv4(o.minAddr[0], o.minAddr[1], o.minAddr[2], o.minAddr[3]) + maxAddr := net.IPv4(o.maxAddr[0], o.maxAddr[1], o.maxAddr[2], o.maxAddr[3]) + return minAddr, maxAddr, o.minPort, o.maxPort + } + return nil, nil, 0, 0 // never hit +} + +func (o *InetAddrRange) Sizeof() uint64 { + return uint64(unsafe.Sizeof(*o)) +} + +func (o *InetAddrRange) Copy(from *InetAddrRange) bool { + if from == nil { + return false + } + copy(o.minAddr[:], from.minAddr[:]) + copy(o.maxAddr[:], from.maxAddr[:]) + o.minPort = from.minPort + o.maxPort = from.maxPort + return true +} + +// IPSetParam mirrors `struct ipset_param` defined in conf/ipset.h +type IPSetParam struct { + kind [IPSET_MAXNAMELEN]byte + name [IPSET_MAXNAMELEN]byte + comment [IPSET_MAXCOMLEN]byte + opcode uint16 + flag uint16 + + // flat reflection of `struct ipset_option`: + // ops create: af(8), comment(8), hashSize(4), maxElem(4) + // ops add: af(8), nomatch(8) + hashSize uint32 + maxElem uint32 + commentOrNomatch uint8 + af uint8 + + proto uint8 + cidr uint8 + addrRange InetAddrRange + iface [unix.IFNAMSIZ]byte + macAddr [6]byte + + // for ipset types with 2 nets + _ uint8 + cidr2 uint8 + addrRange2 InetAddrRange +} + +func (o *IPSetParam) getKind() string { + return string(bytes.TrimRight(o.kind[:], "\x00")) +} + +func (o *IPSetParam) SetKind(kind string) { + if len(kind) > 0 { + copy(o.kind[:], kind) + } +} + +func (o *IPSetParam) SetName(name string) { + if len(name) > 0 { + copy(o.name[:], name) + } +} + +func (o *IPSetParam) SetComment(comment string) { + if len(comment) > 0 { + copy(o.comment[:], comment) + } +} + +func (o *IPSetParam) SetOpcode(opcode uint16) { + o.opcode = opcode +} + +func (o *IPSetParam) SetFlag(flag uint16) { + o.flag = flag +} + +func (o *IPSetParam) AddFlag(flag uint16) { + o.flag |= flag +} + +func (o *IPSetParam) DelFlag(flag uint16) { + o.flag &= ^flag +} + +func (o *IPSetParam) SetAf(af uint8) { + o.af = af +} + +func (o *IPSetParam) SetCommentFlag(enable bool) { + num := 0 + if enable { + num = 1 + } + o.commentOrNomatch = uint8(num) +} + +func (o *IPSetParam) SetNomatch(enable bool) { + num := 0 + if enable { + num = 1 + } + o.commentOrNomatch = uint8(num) +} + +func (o *IPSetParam) SetHashSize(hashSize uint32) { + o.hashSize = hashSize +} + +func (o *IPSetParam) SetMaxElem(maxElem uint32) { + o.maxElem = maxElem +} + +func (o *IPSetParam) SetProto(proto uint8) { + o.proto = proto +} + +func (o *IPSetParam) SetCidr(cidr uint8) { + o.cidr = cidr +} + +func (o *IPSetParam) GetAddrRange() *InetAddrRange { + return &o.addrRange +} + +func (o *IPSetParam) SetIface(iface string) { + if len(iface) > 0 { + copy(o.iface[:], iface) + } +} + +func (o *IPSetParam) SetMacAddr(macAddr string) error { + n, err := fmt.Sscanf(macAddr, "%02x:%02x:%02x:%02x:%02x:%02x", + &o.macAddr[0], &o.macAddr[1], &o.macAddr[2], + &o.macAddr[3], &o.macAddr[4], &o.macAddr[5]) + if err != nil { + return err + } + if n != 6 { + return fmt.Errorf("string macAddr parsed to %d parts, expected 6", n) + } + return nil +} + +func (o *IPSetParam) SetCidr2(cidr uint8) { + o.cidr2 = cidr +} + +func (o *IPSetParam) GetAddrRange2() *InetAddrRange { + return &o.addrRange2 +} + +func (o *IPSetParam) Sizeof() uint64 { + return uint64(unsafe.Sizeof(*o)) +} + +func (o *IPSetParam) Copy(from *IPSetParam) bool { + if from == nil { + return false + } + copy(o.kind[:], from.kind[:]) + copy(o.name[:], from.name[:]) + copy(o.comment[:], from.comment[:]) + o.opcode = from.opcode + o.flag = from.flag + + o.af = from.af + o.commentOrNomatch = from.commentOrNomatch + o.hashSize = from.hashSize + o.maxElem = from.maxElem + + o.proto = from.proto + o.cidr = from.cidr + o.addrRange.Copy(&from.addrRange) + o.iface = from.iface + o.macAddr = from.macAddr + + o.cidr2 = from.cidr2 + o.addrRange2.Copy(&from.addrRange2) + + return true +} + +func (o *IPSetParam) Dump(buf []byte) bool { + var to *IPSetParam + if len(buf) < int(o.Sizeof()) { + return false + } + to = *(**IPSetParam)(unsafe.Pointer(&buf)) + return o.Copy(to) +} + +func (o *IPSetParam) Package() []byte { + buf := new(bytes.Buffer) + binary.Write(buf, binary.LittleEndian, o) + return buf.Bytes() +} + +func (o *IPSetParam) write(conn *pool.Conn) error { + buf := o.Package() + n, err := conn.WriteN(buf, int(o.Sizeof())) + if err != nil { + return fmt.Errorf("IPSetParam write error: %v, %d of %d written\n", + err, n, o.Sizeof()) + } + return nil +} + +type IPSetParamArray []IPSetParam + +// IPSetMember mirrors `struct ipset_meber` defined in conf/ipset.h +type IPSetMember struct { + comment [IPSET_MAXCOMLEN]byte + + addr [16]byte + cidr uint8 + proto uint8 + port uint16 + iface [unix.IFNAMSIZ]byte + macAddr [6]byte + nomatch uint8 + + // for ipset types with 2 nets + cidr2 uint8 + port2 uint16 + _ [2]uint8 + addr2 [16]byte +} + +func (o *IPSetMember) GetComment() string { + return string(bytes.TrimRight(o.comment[:], "\x00")) +} + +func (o *IPSetMember) GetAddr(af uint8) net.IP { + if af == syscall.AF_INET6 { + res := make(net.IP, net.IPv6len) + copy(res, o.addr[:]) + return res + } + return net.IPv4(o.addr[0], o.addr[1], o.addr[2], o.addr[3]) +} + +func (o *IPSetMember) GetCidr() uint8 { + return o.cidr +} + +func (o *IPSetMember) GetProto() uint8 { + return o.proto +} + +func (o *IPSetMember) GetPort() uint16 { + return o.port +} + +func (o *IPSetMember) GetIface() string { + return string(bytes.TrimRight(o.iface[:], "\x00")) +} + +func (o *IPSetMember) GetMacAddr() string { + return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", + o.macAddr[0], o.macAddr[1], o.macAddr[2], + o.macAddr[3], o.macAddr[4], o.macAddr[5]) +} + +func (o *IPSetMember) GetNoMatch() bool { + if o.nomatch > 0 { + return true + } + return false +} + +func (o *IPSetMember) GetCidr2() uint8 { + return o.cidr2 +} + +func (o *IPSetMember) GetPort2() uint16 { + return o.port2 +} + +func (o *IPSetMember) GetAddr2(af uint8) net.IP { + if af == syscall.AF_INET6 { + res := make(net.IP, net.IPv6len) + copy(res, o.addr2[:]) + return res + } + return net.IPv4(o.addr2[0], o.addr2[1], o.addr2[2], o.addr2[3]) +} + +func (o *IPSetMember) Sizeof() uint64 { + return uint64(unsafe.Sizeof(*o)) +} + +func (o *IPSetMember) Copy(from *IPSetMember) bool { + if from == nil { + return false + } + + copy(o.comment[:], from.comment[:]) + + copy(o.addr[:], from.addr[:]) + o.cidr = from.cidr + o.proto = from.proto + o.port = from.port + copy(o.iface[:], from.iface[:]) + copy(o.macAddr[:], from.macAddr[:]) + o.nomatch = from.nomatch + + o.cidr2 = from.cidr2 + o.port2 = from.port2 + copy(o.addr2[:], from.addr2[:]) + + return true +} + +// IPSetInfo mirrors `struct ipset_info` defined in conf/ipset.h +type IPSetInfo struct { + name [IPSET_MAXNAMELEN]byte + kind [IPSET_MAXNAMELEN]byte + comment uint8 + + af uint8 + _ [2]uint8 + + // kind bitmap: cidr(8), addrRange(20) + // kind hash: hashSize(4), hashMaxElem(4) + cidr uint8 + _ [3]uint8 + hashSizeOrAddrRange uint32 + hashMaxElem uint32 + __reserved [28]uint8 + + size uint32 + entries uint32 + references uint32 + + membersPtr uintptr + members []IPSetMember +} + +func (o *IPSetInfo) GetName() string { + return string(bytes.TrimRight(o.name[:], "\x00")) +} + +func (o *IPSetInfo) GetKind() string { + return string(bytes.TrimRight(o.kind[:], "\x00")) +} + +func (o *IPSetInfo) GetComment() bool { + if o.comment > 0 { + return true + } + return false +} + +func (o *IPSetInfo) GetAf() uint8 { + return o.af +} + +func (o *IPSetInfo) GetCidr() uint8 { + return o.cidr +} + +func (o *IPSetInfo) GetAddrRange() (net.IP, net.IP, uint16, uint16) { + iaRange := (*InetAddrRange)(unsafe.Pointer(uintptr(unsafe.Pointer(&o.hashSizeOrAddrRange)))) + return iaRange.Decode(o.af) +} + +func (o *IPSetInfo) GetHashSize() uint32 { + return o.hashSizeOrAddrRange +} + +func (o *IPSetInfo) GetSize() uint32 { + return o.size +} + +func (o *IPSetInfo) GetEntries() uint32 { + return o.entries +} + +func (o *IPSetInfo) GetReferences() uint32 { + return o.references +} + +func (o *IPSetInfo) GetHashMaxElem() uint32 { + return o.hashMaxElem +} + +func (o *IPSetInfo) GetMembers() []IPSetMember { + return o.members +} + +func (o *IPSetInfo) Sizeof() uint64 { + return uint64(unsafe.Offsetof(o.members)) +} + +func (o *IPSetInfo) Copy(from *IPSetInfo) bool { + if from == nil { + return false + } + + copy(o.name[:], from.name[:]) + copy(o.kind[:], from.kind[:]) + o.comment = from.comment + + o.af = from.af + + o.cidr = from.cidr + o.hashSizeOrAddrRange = from.hashSizeOrAddrRange + o.hashMaxElem = from.hashMaxElem + copy(o.__reserved[:], from.__reserved[:]) + + o.size = from.size + o.entries = from.entries + o.references = from.references + + //// Note: + //// Do NOT copy members! They are not in C struct. + // o.members = make([]IPSetMember, len(from.members)) + // for i, _ := range from.members { + // o.members[i].Copy(&from.members[i]) + // } + + return true +} + +// IPSetInfoArray interprets `struct ipset_info_array` defined in conf/ipset.h +type IPSetInfoArray struct { + infos []IPSetInfo +} + +func (o *IPSetInfoArray) GetIPSetInfos() []IPSetInfo { + return o.infos +} + +func (o *IPSetInfoArray) read(conn *pool.Conn, logger hclog.Logger) error { + var info *IPSetInfo + var member *IPSetMember + var i, j, nipset uint32 + var offset uint64 + + dataLen := uint64(unsafe.Sizeof(nipset)) + buf, err := conn.ReadN(int(dataLen)) + if err != nil { + return fmt.Errorf("Read IPSetInfo number failed: %v", err) + } + nipset = binary.LittleEndian.Uint32(buf[:dataLen]) + if nipset == 0 { + return nil + } + + // read IPSetInfo data + dataLen = (uint64(nipset)) * info.Sizeof() + buf, err = conn.ReadN(int(dataLen)) + if err != nil { + return fmt.Errorf("Read IPSetInfo data failed: %v", err) + } + + dataLen = 0 + offset = 0 + o.infos = make([]IPSetInfo, nipset) + for i = 0; i < nipset; i++ { + info = (*IPSetInfo)(unsafe.Pointer(uintptr(unsafe.Pointer(&buf[offset])))) + o.infos[i].Copy(info) + offset += info.Sizeof() + dataLen += uint64(info.entries) * member.Sizeof() + } + if dataLen == 0 { + return nil + } + + // read IPSetMember data + buf, err = conn.ReadN(int(dataLen)) + if err != nil { + return fmt.Errorf("Read IPSetMember data failed: %v", err) + } + offset = 0 + for i = 0; i < nipset; i++ { + o.infos[i].members = make([]IPSetMember, o.infos[i].entries) + for j = 0; j < o.infos[i].entries; j++ { + member = (*IPSetMember)(unsafe.Pointer(uintptr(unsafe.Pointer(&buf[offset])))) + o.infos[i].members[j].Copy(member) + offset += member.Sizeof() + } + } + + return nil +} + +type CheckResult int32 + +func (o *CheckResult) Sizeof() uint64 { + return uint64(unsafe.Sizeof(*o)) +} + +func (o *CheckResult) Dump(buf []byte) bool { + if len(buf) != int(o.Sizeof()) { + return false + } + reader := bytes.NewReader(buf) + if err := binary.Read(reader, binary.LittleEndian, o); err != nil { + return false + } + return true +} + +func (o *CheckResult) read(conn *pool.Conn, logger hclog.Logger) error { + buf, err := conn.ReadN(int(o.Sizeof())) + if err != nil { + return fmt.Errorf("Read ipset check result failed: %v", err) + } + if o.Dump(buf) != true { + return fmt.Errorf("Dump ipset check result failed") + } + return nil +} + +func getLogger(name string, parent hclog.Logger) hclog.Logger { + if parent != nil { + return parent.Named(name) + } + return hclog.Default().Named(name) +} + +func (o *IPSetParam) Get(cp *pool.ConnPool, parentLogger hclog.Logger) (*IPSetInfoArray, error, DpvsErrType) { + logger := getLogger("ipset:get", parentLogger) + + if o.opcode != IPSET_OP_LIST { + logger.Error("Invalid ipset opcode for Get", "opcode", o.opcode) + return nil, fmt.Errorf("invalid ipset opcode %d for get", o.opcode), 0 + } + + ctx := context.Background() + conn, err := cp.Get(ctx) + if err != nil { + logger.Error("Get conn from pool failed", "Error", err.Error()) + return nil, err, 0 + } + defer cp.Remove(ctx, conn, nil) + + msg := NewSockMsg(SOCKOPT_VERSION, SOCKOPT_GET_IPSET_LIST, SOCKOPT_GET, o.Sizeof()) + if err = msg.Write(conn); err != nil { + logger.Error("SOCKOPT_GET_IPSET_LIST write proto header failed", "Error", err.Error()) + return nil, err, 0 + } + + if err = o.write(conn); err != nil { + logger.Error("SOCKOPT_GET_IPSET_LIST write ipset param failed", "Error", err.Error()) + return nil, err, 0 + } + + reply := NewReplySockMsg() + if err = reply.Read(conn); err != nil { + logger.Error("SOCKOPT_GET_IPSET_LIST read reply header failed", "Error", err.Error()) + return nil, err, 0 + } + if reply.GetErrCode() != EDPVS_OK { + errStr := reply.GetErrStr() + logger.Error("SOCKOPT_GET_IPSET_LIST replied error", "DPVS.Error", errStr) + return nil, fmt.Errorf("DPVS Response Error: %s", errStr), reply.GetErrCode() + } + + output := &IPSetInfoArray{} + if reply.GetLen() > 0 { + err = output.read(conn, logger) + if err != nil { + logger.Error("SOCKOPT_GET_IPSET_LIST read reply data failed", "Error", err.Error()) + return nil, err, 0 + } + } + return output, nil, 0 +} + +func (o *IPSetParam) CreateDestroy(cp *pool.ConnPool, parentLogger hclog.Logger) (error, DpvsErrType) { + if o.opcode != IPSET_OP_CREATE && o.opcode != IPSET_OP_DESTROY { + return fmt.Errorf("invalid ipset opcode %d for Create/Destroy", o.opcode), 0 + } + logName := "ipset:create" + if o.opcode == IPSET_OP_DESTROY { + logName = "ipset:destroy" + } + logger := getLogger(logName, parentLogger) + + ctx := context.Background() + conn, err := cp.Get(ctx) + if err != nil { + logger.Error("Get conn from pool failed", "Error", err.Error()) + return err, 0 + } + defer cp.Remove(ctx, conn, nil) + + msg := NewSockMsg(SOCKOPT_VERSION, SOCKOPT_SET_IPSET, SOCKOPT_SET, o.Sizeof()) + if err = msg.Write(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET write proto header failed", "Error", err.Error()) + return err, 0 + } + + if err = o.write(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET write ipset param failed", "Error", err.Error()) + return err, 0 + } + + reply := NewReplySockMsg() + if err = reply.Read(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET read reply header failed", "Error", err.Error()) + return err, 0 + } + dpvsErrCode := reply.GetErrCode() + if dpvsErrCode != EDPVS_OK { + /* + if !(dpvsErrCode == EDPVS_EXIST && o.opcode == IPSET_OP_CREATE || + dpvsErrCode == EDPVS_NOTEXIST && o.opcode == IPSET_OP_DESTROY) { + errStr := reply.GetErrStr() + logger.Error("SOCKOPT_SET_IPSET replied error", "DPVS.Error", errStr) + return fmt.Errorf("DPVS Response Error: %s", errStr), reply.GetErrCode() + } + */ + errStr := reply.GetErrStr() + logger.Error("SOCKOPT_SET_IPSET replied error", "DPVS.Error", errStr) + return fmt.Errorf("DPVS Response Error: %s", errStr), reply.GetErrCode() + } + return nil, 0 +} + +func (o *IPSetParam) IsIn(cp *pool.ConnPool, parentLogger hclog.Logger) (bool, error, DpvsErrType) { + logger := getLogger("ipset:isin", parentLogger) + + result := false + if o.opcode != IPSET_OP_TEST { + logger.Error("Invalid ipset opcode for TEST", "opcode", o.opcode) + return result, fmt.Errorf("invalid ipset opcode %d for TEST", o.opcode), 0 + } + + ctx := context.Background() + conn, err := cp.Get(ctx) + if err != nil { + logger.Error("Get conn from pool failed", "Error", err.Error()) + return result, err, 0 + } + defer cp.Remove(ctx, conn, nil) + + msg := NewSockMsg(SOCKOPT_VERSION, SOCKOPT_GET_IPSET_TEST, SOCKOPT_GET, o.Sizeof()) + if err = msg.Write(conn); err != nil { + logger.Error("SOCKOPT_GET_IPSET_TEST write proto header failed", "Error", err.Error()) + return result, err, 0 + } + + if err = o.write(conn); err != nil { + logger.Error("SOCKOPT_GET_IPSET_TEST write ipset param failed", "Error", err.Error()) + return result, err, 0 + } + + reply := NewReplySockMsg() + if err = reply.Read(conn); err != nil { + logger.Error("SOCKOPT_GET_IPSET_TEST read reply header failed", "Error", err.Error()) + return result, err, 0 + } + if reply.GetErrCode() != EDPVS_OK { + errStr := reply.GetErrStr() + logger.Error("SOCKOPT_GET_IPSET_TEST replied error", "DPVS.Error", errStr) + return result, fmt.Errorf("DPVS Response Error: %s", errStr), reply.GetErrCode() + } + + var output CheckResult + err = output.read(conn, logger) + if err != nil { + logger.Error("SOCKOPT_GET_IPSET_LIST read reply data failed", "Error", err.Error()) + return result, err, 0 + } + if output > 0 { + result = true + } + return result, nil, 0 +} + +func (o *IPSetParamArray) AddDelMember(cp *pool.ConnPool, parentLogger hclog.Logger) (error, DpvsErrType) { + if len(*o) == 0 { + return nil, 0 + } + opcode := (*o)[0].opcode + if opcode != IPSET_OP_ADD && opcode != IPSET_OP_DEL { + return fmt.Errorf("invalid ipset opcode %d for Add/Del", opcode), 0 + } + name := (*o)[0].name + for _, param := range *o { + if opcode != param.opcode { + return fmt.Errorf("ipset opcode in param array did not match for Add/Del"), 0 + } + if !bytes.Equal(name[:], param.name[:]) { + return fmt.Errorf("ipset name in param array did not match for Add/Del"), 0 + } + } + + logName := "ipset:add" + if opcode == IPSET_OP_DEL { + logName = "ipset:del" + } + logger := getLogger(logName, parentLogger) + + for _, param := range *o { + ctx := context.Background() + conn, err := cp.Get(ctx) + if err != nil { + logger.Error("Get conn from pool failed", "Error", err.Error()) + return err, 0 + } + + msg := NewSockMsg(SOCKOPT_VERSION, SOCKOPT_SET_IPSET, SOCKOPT_SET, param.Sizeof()) + if err = msg.Write(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET write proto header failed", "Error", err.Error()) + cp.Remove(ctx, conn, nil) + return err, 0 + } + + if err = param.write(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET write ipset param failed", "Error", err.Error()) + cp.Remove(ctx, conn, nil) + return err, 0 + } + + reply := NewReplySockMsg() + if err = reply.Read(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET read reply header failed", "Error", err.Error()) + cp.Remove(ctx, conn, nil) + return err, 0 + } + cp.Remove(ctx, conn, nil) + + dpvsErrCode := reply.GetErrCode() + if dpvsErrCode != EDPVS_OK { + /* + if dpvsErrCode == EDPVS_EXIST && opcode == IPSET_OP_ADD || + dpvsErrCode == EDPVS_NOTEXIST && opcode == IPSET_OP_DEL { + continue + } + */ + errStr := reply.GetErrStr() + logger.Error("SOCKOPT_SET_IPSET replied error", "DPVS.Error", errStr) + return fmt.Errorf("DPVS Response Error: %s", errStr), reply.GetErrCode() + } + } + return nil, 0 +} + +func (o *IPSetParamArray) ReplaceMember(cp *pool.ConnPool, parentLogger hclog.Logger) (error, DpvsErrType) { + if len(*o) == 0 { + return nil, 0 + } + opcode := (*o)[0].opcode + if opcode != IPSET_OP_ADD && opcode != IPSET_OP_FLUSH { + return fmt.Errorf("invalid ipset opcode %d for Replace", opcode), 0 + } + name := (*o)[0].name + for i, param := range *o { + if i == 0 { + continue + } + if opcode != param.opcode { + return fmt.Errorf("ipset opcode in param array did not match for Replace"), 0 + } + if !bytes.Equal(name[:], param.name[:]) { + return fmt.Errorf("ipset name in param array did not match for Repalce"), 0 + } + } + + logger := getLogger("replace", parentLogger) + + // Flush the whole ipset + param := &IPSetParam{} + param.Copy(&(*o)[0]) + param.opcode = IPSET_OP_FLUSH + + ctx := context.Background() + conn, err := cp.Get(ctx) + if err != nil { + logger.Error("Get conn from pool failed", "Error", err.Error()) + return err, 0 + } + + msg := NewSockMsg(SOCKOPT_VERSION, SOCKOPT_SET_IPSET, SOCKOPT_SET, param.Sizeof()) + if err = msg.Write(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET write proto header failed", "Error", err.Error()) + cp.Remove(ctx, conn, nil) + return err, 0 + } + + if err = param.write(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET write ipset param failed", "Error", err.Error()) + cp.Remove(ctx, conn, nil) + return err, 0 + } + + reply := NewReplySockMsg() + if err = reply.Read(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET read reply header failed", "Error", err.Error()) + cp.Remove(ctx, conn, nil) + return err, 0 + } + cp.Remove(ctx, conn, nil) + + if reply.GetErrCode() != EDPVS_OK { + errStr := reply.GetErrStr() + logger.Error("SOCKOPT_SET_IPSET replied error", "DPVS.Error", errStr) + return fmt.Errorf("DPVS Response Error: %s", errStr), reply.GetErrCode() + } + + if opcode != IPSET_OP_ADD { + return nil, 0 + } + + // Add members into ipset + for _, param := range *o { + ctx := context.Background() + conn, err := cp.Get(ctx) + if err != nil { + logger.Error("Get conn from pool failed", "Error", err.Error()) + return err, 0 + } + + msg := NewSockMsg(SOCKOPT_VERSION, SOCKOPT_SET_IPSET, SOCKOPT_SET, param.Sizeof()) + if err = msg.Write(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET write proto header failed", "Error", err.Error()) + cp.Remove(ctx, conn, nil) + return err, 0 + } + + if err = param.write(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET write ipset param failed", "Error", err.Error()) + cp.Remove(ctx, conn, nil) + return err, 0 + } + + reply := NewReplySockMsg() + if err = reply.Read(conn); err != nil { + logger.Error("SOCKOPT_SET_IPSET read reply header failed", "Error", err.Error()) + cp.Remove(ctx, conn, nil) + return err, 0 + } + cp.Remove(ctx, conn, nil) + + dpvsErrCode := reply.GetErrCode() + if dpvsErrCode != EDPVS_OK { + errStr := reply.GetErrStr() + logger.Error("SOCKOPT_SET_IPSET replied error", "DPVS.Error", errStr) + return fmt.Errorf("DPVS Response Error: %s", errStr), reply.GetErrCode() + } + } + return nil, 0 +} diff --git a/tools/dpvs-agent/pkg/ipc/types/ipset_models.go b/tools/dpvs-agent/pkg/ipc/types/ipset_models.go new file mode 100644 index 000000000..203cad130 --- /dev/null +++ b/tools/dpvs-agent/pkg/ipc/types/ipset_models.go @@ -0,0 +1,1305 @@ +// Copyright 2023 IQiYi Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "encoding/binary" + "fmt" + "net" + "strconv" + "strings" + "syscall" + "unicode" + + "github.com/dpvs-agent/models" +) + +var ( + _ IPSetType = (*IPSetBitmapIP)(nil) + _ IPSetType = (*IPSetBitmapIPMac)(nil) + _ IPSetType = (*IPSetBitmapPort)(nil) + _ IPSetType = (*IPSetHashIP)(nil) + _ IPSetType = (*IPSetHashNet)(nil) + _ IPSetType = (*IPSetHashIPPort)(nil) + _ IPSetType = (*IPSetHashNetPort)(nil) + _ IPSetType = (*IPSetHashNetPortIface)(nil) + _ IPSetType = (*IPSetHashIPPortIP)(nil) + _ IPSetType = (*IPSetHashIPPortNet)(nil) + _ IPSetType = (*IPSetHashNetPortNet)(nil) + _ IPSetType = (*IPSetHashNetPortNetPort)(nil) +) + +type IPSetType interface { + // Update IPSetParam with parsed fields from models.IpsetMember.Entry + ParseEntry(string, *IPSetParam) error + // Create a models.IpsetMember with Entry field filled + ModelEntry(uint8, *IPSetMember) (*models.IpsetMember, error) + // Check if IPSetParam is valid + CheckParam(*IPSetParam) error +} + +type IPSetBitmapIP struct{} +type IPSetBitmapIPMac struct{} +type IPSetBitmapPort struct{} +type IPSetHashIP struct{} +type IPSetHashNet struct{} +type IPSetHashIPPort struct{} +type IPSetHashNetPort struct{} +type IPSetHashNetPortIface struct{} +type IPSetHashIPPortIP struct{} +type IPSetHashIPPortNet struct{} +type IPSetHashNetPortNet struct{} +type IPSetHashNetPortNetPort struct{} + +func (o *IPSetBitmapIP) ParseEntry(entry string, param *IPSetParam) error { + startIP, endIP, af, pfx, err := parseAddrRange(entry) + if err != nil { + return fmt.Errorf("Parse models.IpsetMember Entry failed: %v", err) + } + + param.SetAf(af) + if startIP != nil { + param.GetAddrRange().SetMinAddr(startIP) + } + if endIP != nil { + param.GetAddrRange().SetMaxAddr(endIP) + } else { + param.GetAddrRange().SetMaxAddr(startIP) + } + param.SetCidr(pfx) + + return nil +} + +func (o *IPSetBitmapIP) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + entry := "" + + if member.GetCidr() > 0 { + entry = fmt.Sprintf("%s/%d", member.GetAddr(af), member.GetCidr()) + } else { + entry = fmt.Sprintf("%s", member.GetAddr(af)) + } + model.Entry = &entry + + return model, nil +} + +func (o *IPSetBitmapIP) CheckParam(param *IPSetParam) error { + if param.af == syscall.AF_INET6 { + return fmt.Errorf("bitmap:ip doesn't support ipv6") + } + if param.opcode != IPSET_OP_CREATE { + return nil + } + if param.cidr > 0 { + if param.cidr < 16 { + return fmt.Errorf("bitmap:ip net seg too big, cidr should be no smaller than 16") + } + return nil + } + if param.af == syscall.AF_INET { + startIP, endIP, _, _ := param.addrRange.Decode(param.af) + if ip4ToUint32(startIP) >= ip4ToUint32(endIP) { + return fmt.Errorf("bitmap:ip requires a network range or cidr") + } + } + return nil +} + +func (o *IPSetBitmapIPMac) ParseEntry(entry string, param *IPSetParam) error { + segs := strings.Split(entry, ",") + + startIP, endIP, af, pfx, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range %s", segs[0]) + } + param.SetAf(af) + if startIP != nil { + param.GetAddrRange().SetMinAddr(startIP) + } + if endIP != nil { + param.GetAddrRange().SetMaxAddr(endIP) + } else { + param.GetAddrRange().SetMaxAddr(startIP) + } + param.SetCidr(pfx) + + if len(segs) > 1 { + if err := param.SetMacAddr(segs[1]); err != nil { + return fmt.Errorf("invalid mac address: %s", err.Error()) + } + } + + return nil +} + +func (o *IPSetBitmapIPMac) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + entry := fmt.Sprintf("%s,%s", member.GetAddr(af), member.GetMacAddr()) + model.Entry = &entry + + return model, nil +} + +func (o *IPSetBitmapIPMac) CheckParam(param *IPSetParam) error { + if param.af == syscall.AF_INET6 { + return fmt.Errorf("bitmap:ip,mac doesn't support ipv6") + } + if param.opcode != IPSET_OP_CREATE { + if param.cidr > 0 { + return fmt.Errorf("bitmap:ip,mac doesn't support addr cidr") + } + if param.af == syscall.AF_INET { + startIP, endIP, _, _ := param.addrRange.Decode(param.af) + if endIP != nil && !endIP.Equal(startIP) { + return fmt.Errorf("bitmap:ip,mac doesn't support addr range") + } + } + } else { + if param.cidr > 0 { + if param.cidr < 16 { + return fmt.Errorf("bitmap:ip,mac net seg too big, cidr should be no smaller than 16") + } + return nil + } + if param.af == syscall.AF_INET { + startIP, endIP, _, _ := param.addrRange.Decode(param.af) + if ip4ToUint32(startIP) >= ip4ToUint32(endIP) { + return fmt.Errorf("bitmap:ip,mac create requires a network range or cidr") + } + } + } + return nil +} + +func (o *IPSetBitmapPort) ParseEntry(entry string, param *IPSetParam) error { + startPort, endPort, proto, err := parsePortRange(entry) + if err != nil { + return err + } + param.GetAddrRange().SetMinPort(startPort) + if endPort > 0 { + param.GetAddrRange().SetMaxPort(endPort) + } else { + param.GetAddrRange().SetMaxPort(startPort) + } + param.SetProto(proto) + return nil +} + +func (o *IPSetBitmapPort) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + + entry := fmt.Sprintf("%s:%d", protoString(member.GetProto()), member.GetPort()) + model.Entry = &entry + + return model, nil +} + +func (o *IPSetBitmapPort) CheckParam(param *IPSetParam) error { + if param.opcode == IPSET_OP_CREATE { + if param.proto != 0 { + return fmt.Errorf("bitmap:port doesn't support proto in create") + } + } else { + if param.addrRange.minPort > 0 && + param.proto != syscall.IPPROTO_TCP && param.proto != syscall.IPPROTO_UDP { + return fmt.Errorf("invalid bitmap:port protocol %s", protoString(param.proto)) + } + } + return nil +} + +func (o *IPSetHashIP) ParseEntry(entry string, param *IPSetParam) error { + startIP, endIP, af, pfx, err := parseAddrRange(entry) + if err != nil { + return fmt.Errorf("invalid addr range %s", entry) + } + param.SetAf(af) + if startIP != nil { + param.GetAddrRange().SetMinAddr(startIP) + } + if endIP != nil { + param.GetAddrRange().SetMaxAddr(endIP) + } else { + param.GetAddrRange().SetMaxAddr(startIP) + } + param.SetCidr(pfx) + return nil +} + +func (o *IPSetHashIP) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + + entry := member.GetAddr(af).String() + model.Entry = &entry + + return model, nil +} + +func (o *IPSetHashIP) CheckParam(param *IPSetParam) error { + if param.opcode != IPSET_OP_ADD && param.opcode != IPSET_OP_DEL { + return nil + } + if param.af == syscall.AF_INET6 { + if param.cidr > 0 { + return fmt.Errorf("hash:ip doesn't support IPv6 cidr") + } + } else if param.af == syscall.AF_INET { + if param.cidr > 0 && param.cidr < 16 { + return fmt.Errorf("ipv4 address cidr range too big, 65536 at most") + } + } + startIP, endIP, _, _ := param.addrRange.Decode(param.af) + if param.af == syscall.AF_INET { + startIPNum, endIPNum := ip4ToUint32(startIP), ip4ToUint32(endIP) + if endIPNum > 0 && endIPNum-startIPNum >= 65535 { + return fmt.Errorf("ipv4 address range too big, 65536 at most") + } + } + return nil +} + +func (o *IPSetHashNet) ParseEntry(entry string, param *IPSetParam) error { + // the same as IPSetHashIP + var iphash IPSetHashIP + return iphash.ParseEntry(entry, param) +} + +func (o *IPSetHashNet) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + + entry := fmt.Sprintf("%s/%d", member.GetAddr(af).String(), member.GetCidr()) + model.Entry = &entry + + return model, nil +} + +func (o *IPSetHashNet) CheckParam(param *IPSetParam) error { + // nothing to do + return nil +} + +func (o *IPSetHashIPPort) ParseEntry(entry string, param *IPSetParam) error { + segs := strings.Split(entry, ",") + + startIP, endIP, af, pfx, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range %s", segs[0]) + } + param.SetAf(af) + if startIP != nil { + param.GetAddrRange().SetMinAddr(startIP) + } + if endIP != nil { + param.GetAddrRange().SetMaxAddr(endIP) + } else { + param.GetAddrRange().SetMaxAddr(startIP) + } + param.SetCidr(pfx) + + if len(segs) > 1 { + startPort, endPort, proto, err := parsePortRange(segs[1]) + if err != nil { + return err + } + param.GetAddrRange().SetMinPort(startPort) + if endPort > 0 { + param.GetAddrRange().SetMaxPort(endPort) + } else { + param.GetAddrRange().SetMaxPort(startPort) + } + param.SetProto(proto) + } + + return nil +} + +func (o *IPSetHashIPPort) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + + entry := fmt.Sprintf("%s,%s:%d", + member.GetAddr(af).String(), + protoString(member.GetProto()), + member.GetPort()) + model.Entry = &entry + + return model, nil +} + +func (o *IPSetHashIPPort) CheckParam(param *IPSetParam) error { + if param.opcode != IPSET_OP_ADD && param.opcode != IPSET_OP_DEL { + return nil + } + if param.af == syscall.AF_INET6 { + if param.cidr > 0 { + return fmt.Errorf("hash:ip,port doesn't support IPv6 cidr") + } + } else if param.af == syscall.AF_INET { + if param.cidr > 0 && param.cidr < 24 { + return fmt.Errorf("ipv4 address cidr range too big, 256 at most") + } + } + + startIP, endIP, startPort, endPort := param.addrRange.Decode(param.af) + if param.af == syscall.AF_INET { + startIPNum, endIPNum := ip4ToUint32(startIP), ip4ToUint32(endIP) + if endIPNum > 0 && endIPNum-startIPNum >= 256 { + return fmt.Errorf("ipv4 address range too big, 256 at most") + } + } + if endPort > 0 && endPort-startPort >= 256 { + return fmt.Errorf("port range too big, 256 at most") + } + return nil +} + +func (o *IPSetHashNetPort) ParseEntry(entry string, param *IPSetParam) error { + // the same as IPSetHashIPPort + var ipport IPSetHashIPPort + return ipport.ParseEntry(entry, param) +} + +func (o *IPSetHashNetPort) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + + entry := fmt.Sprintf("%s/%d,%s:%d", + member.GetAddr(af).String(), + member.GetPort(), + protoString(member.GetProto()), + member.GetPort()) + model.Entry = &entry + + return model, nil +} + +func (o *IPSetHashNetPort) CheckParam(param *IPSetParam) error { + // nothing to do + return nil +} + +func (o *IPSetHashNetPortIface) ParseEntry(entry string, param *IPSetParam) error { + segs := strings.Split(entry, ",") + if len(segs) < 3 { + return fmt.Errorf("invalid hash:net,port,iface entry: %s", entry) + } + + startIP, endIP, af, pfx, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range %s, error %v", segs[0], err) + } + param.SetAf(af) + if startIP != nil { + param.GetAddrRange().SetMinAddr(startIP) + } + if endIP != nil { + param.GetAddrRange().SetMaxAddr(endIP) + } else { + param.GetAddrRange().SetMaxAddr(startIP) + } + param.SetCidr(pfx) + + segs = segs[1:] + startPort, endPort, proto, err := parsePortRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid port range %s, error %v", segs[0], err) + } + param.GetAddrRange().SetMinPort(startPort) + if endPort > 0 { + param.GetAddrRange().SetMaxPort(endPort) + } else { + param.GetAddrRange().SetMaxPort(startPort) + } + param.SetProto(proto) + + segs = segs[1:] + if len(segs[0]) == 0 { + return fmt.Errorf("empty interface name") + } + param.SetIface(segs[0]) + + return nil +} + +func (o *IPSetHashNetPortIface) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + + entry := fmt.Sprintf("%s/%d,%s:%d,%s", + member.GetAddr(af).String(), + member.GetPort(), + protoString(member.GetProto()), + member.GetPort(), + member.GetIface()) + model.Entry = &entry + + return model, nil +} + +func (o *IPSetHashNetPortIface) CheckParam(param *IPSetParam) error { + // nothing to do + return nil +} + +func (o *IPSetHashIPPortIP) ParseEntry(entry string, param *IPSetParam) error { + segs := strings.Split(entry, ",") + if len(segs) < 3 { + return fmt.Errorf("invalid hash:ip,port,ip entry: %s", entry) + } + + startIP, endIP, af, pfx, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range %s, error %v", segs[0], err) + } + param.SetAf(af) + if startIP != nil { + param.GetAddrRange().SetMinAddr(startIP) + } + if endIP != nil { + param.GetAddrRange().SetMaxAddr(endIP) + } else { + param.GetAddrRange().SetMaxAddr(startIP) + } + param.SetCidr(pfx) + + segs = segs[1:] + startPort, endPort, proto, err := parsePortRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid port range %s, err %v", segs[0], err) + } + param.GetAddrRange().SetMinPort(startPort) + if endPort > 0 { + param.GetAddrRange().SetMaxPort(endPort) + } else { + param.GetAddrRange().SetMaxPort(startPort) + } + param.SetProto(proto) + + segs = segs[1:] + startIP2, endIP2, af2, pfx2, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range2 %s, error %v", segs[0], err) + } + if af2 != af { + return fmt.Errorf("address family mismatch in hash:ip,port,ip member") + } + if startIP2 != nil { + param.GetAddrRange2().SetMinAddr(startIP2) + } + if endIP2 != nil { + param.GetAddrRange2().SetMaxAddr(endIP2) + } else { + param.GetAddrRange2().SetMaxAddr(startIP2) + } + param.SetCidr2(pfx2) + + return nil +} + +func (o *IPSetHashIPPortIP) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + + entry := fmt.Sprintf("%s,%s:%d,%s", + member.GetAddr(af).String(), + protoString(member.GetProto()), + member.GetPort(), + member.GetAddr2(af).String()) + model.Entry = &entry + + return model, nil +} + +func (o *IPSetHashIPPortIP) CheckParam(param *IPSetParam) error { + if param.opcode != IPSET_OP_ADD && param.opcode != IPSET_OP_DEL { + return nil + } + + if param.af == syscall.AF_INET6 { + if param.cidr > 0 || param.cidr2 > 0 { + return fmt.Errorf("hash:ip,port,ip doesn't support IPv6 cidr") + } + } else if param.af == syscall.AF_INET { + if param.cidr > 0 && param.cidr < 24 { + return fmt.Errorf("ipv4 address cidr range too big, 256 at most") + } + if param.cidr2 > 0 && param.cidr2 < 24 { + return fmt.Errorf("ipv4 address cidr2 range too big, 256 at most") + } + } + + startIP, endIP, startPort, endPort := param.addrRange.Decode(param.af) + if param.af == syscall.AF_INET { + startIPNum, endIPNum := ip4ToUint32(startIP), ip4ToUint32(endIP) + if endIPNum > 0 && endIPNum-startIPNum >= 256 { + return fmt.Errorf("ipv4 address range too big, 256 at most") + } + } + if endPort > 0 && endPort-startPort >= 256 { + return fmt.Errorf("port range too big, 256 at most") + } + + startIP2, endIP2, _, _ := param.addrRange.Decode(param.af) + if param.af == syscall.AF_INET { + startIPNum2, endIPNum2 := ip4ToUint32(startIP2), ip4ToUint32(endIP2) + if endIPNum2 > 0 && endIPNum2-startIPNum2 >= 256 { + return fmt.Errorf("ipv4 address range2 too big, 256 at most") + } + } + + return nil +} + +func (o *IPSetHashIPPortNet) ParseEntry(entry string, param *IPSetParam) error { + segs := strings.Split(entry, ",") + if len(segs) < 3 { + return fmt.Errorf("invalid hash:ip,port,net entry: %s", entry) + } + + // Notes: The "ip" and "net" parts in hash:ip,port,net corresponds to addr range1 + // and addr range2 respectively, so that match from a source address network + // to a single dest address can be implemented easily. + + // the "ip" part corresponds to address range2 + startIP2, endIP2, af2, pfx2, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range %s, error %v", segs[0], err) + } + param.SetAf(af2) + if startIP2 != nil { + param.GetAddrRange2().SetMinAddr(startIP2) + } + if endIP2 != nil { + param.GetAddrRange2().SetMaxAddr(endIP2) + } else { + param.GetAddrRange2().SetMaxAddr(startIP2) + } + param.SetCidr2(pfx2) + + // the "port" part + segs = segs[1:] + startPort, endPort, proto, err := parsePortRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid port range %s, err %v", segs[0], err) + } + param.GetAddrRange().SetMinPort(startPort) + if endPort > 0 { + param.GetAddrRange().SetMaxPort(endPort) + } else { + param.GetAddrRange().SetMaxPort(startPort) + } + param.SetProto(proto) + + // the "net" part corresponds to address range1 + segs = segs[1:] + startIP, endIP, af, pfx, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range %s, error %v", segs[0], err) + } + if af != af2 { + return fmt.Errorf("address family mismatch in hash:ip,port,net member") + } + if startIP != nil { + param.GetAddrRange().SetMinAddr(startIP) + } + if endIP != nil { + param.GetAddrRange().SetMaxAddr(endIP) + } else { + param.GetAddrRange().SetMaxAddr(startIP) + } + param.SetCidr(pfx) + + return nil +} + +func (o *IPSetHashIPPortNet) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + + entry := fmt.Sprintf("%s/%d,%s:%d,%s", + member.GetAddr(af).String(), + member.GetCidr(), + protoString(member.GetProto()), + member.GetPort(), + member.GetAddr2(af).String()) + model.Entry = &entry + + return model, nil +} + +func (o *IPSetHashIPPortNet) CheckParam(param *IPSetParam) error { + if param.opcode != IPSET_OP_ADD && param.opcode != IPSET_OP_DEL { + return nil + } + + if param.af == syscall.AF_INET6 { + if param.cidr2 > 0 { + return fmt.Errorf("hash:ip,port,net doesn't support IPv6 cidr") + } + } else if param.af == syscall.AF_INET { + if param.cidr2 > 0 && param.cidr2 < 24 { + return fmt.Errorf("ipv4 address cidr2 range too big, 256 at most") + } + } + + _, _, startPort, endPort := param.addrRange.Decode(param.af) + if endPort > 0 && endPort-startPort >= 256 { + return fmt.Errorf("port range too big, 256 at most") + } + + startIP2, endIP2, _, _ := param.addrRange.Decode(param.af) + if param.af == syscall.AF_INET { + startIPNum2, endIPNum2 := ip4ToUint32(startIP2), ip4ToUint32(endIP2) + if endIPNum2 > 0 && endIPNum2-startIPNum2 >= 256 { + return fmt.Errorf("ipv4 address range2 too big, 256 at most") + } + } + + return nil +} + +func (o *IPSetHashNetPortNet) ParseEntry(entry string, param *IPSetParam) error { + // Notes: almost the same as IPSetHashIPPortIP except the error message, + + segs := strings.Split(entry, ",") + if len(segs) < 3 { + return fmt.Errorf("invalid hash:net,port,net entry: %s", entry) + } + + startIP, endIP, af, pfx, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range %s, error %v", segs[0], err) + } + param.SetAf(af) + if startIP != nil { + param.GetAddrRange().SetMinAddr(startIP) + } + if endIP != nil { + param.GetAddrRange().SetMaxAddr(endIP) + } else { + param.GetAddrRange().SetMaxAddr(startIP) + } + param.SetCidr(pfx) + + segs = segs[1:] + startPort, endPort, proto, err := parsePortRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid port range %s, err %v", segs[0], err) + } + param.GetAddrRange().SetMinPort(startPort) + if endPort > 0 { + param.GetAddrRange().SetMaxPort(endPort) + } else { + param.GetAddrRange().SetMaxPort(startPort) + } + param.SetProto(proto) + + segs = segs[1:] + startIP2, endIP2, af2, pfx2, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range2 %s, error %v", segs[0], err) + } + if af2 != af { + return fmt.Errorf("address family mismatch in hash:net,port,net member") + } + if startIP2 != nil { + param.GetAddrRange2().SetMinAddr(startIP2) + } + if endIP2 != nil { + param.GetAddrRange2().SetMaxAddr(endIP2) + } else { + param.GetAddrRange2().SetMaxAddr(startIP2) + } + param.SetCidr2(pfx2) + + return nil +} + +func (o *IPSetHashNetPortNet) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + + entry := fmt.Sprintf("%s/%d,%s:%d,%s/%d", + member.GetAddr(af).String(), + member.GetCidr(), + protoString(member.GetProto()), + member.GetPort(), + member.GetAddr2(af).String(), + member.GetCidr2()) + model.Entry = &entry + + return model, nil +} + +func (o *IPSetHashNetPortNet) CheckParam(param *IPSetParam) error { + // nothing to do + return nil +} + +func (o *IPSetHashNetPortNetPort) ParseEntry(entry string, param *IPSetParam) error { + segs := strings.Split(entry, ",") + if len(segs) < 4 { + return fmt.Errorf("invalid hash:net,port,net,port entry: %s", entry) + } + + startIP, endIP, af, pfx, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range %s, error %v", segs[0], err) + } + param.SetAf(af) + if startIP != nil { + param.GetAddrRange().SetMinAddr(startIP) + } + if endIP != nil { + param.GetAddrRange().SetMaxAddr(endIP) + } else { + param.GetAddrRange().SetMaxAddr(startIP) + } + param.SetCidr(pfx) + + segs = segs[1:] + startPort, endPort, proto, err := parsePortRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid port range %s, err %v", segs[0], err) + } + param.GetAddrRange().SetMinPort(startPort) + if endPort > 0 { + param.GetAddrRange().SetMaxPort(endPort) + } else { + param.GetAddrRange().SetMaxPort(startPort) + } + param.SetProto(proto) + + segs = segs[1:] + startIP2, endIP2, af2, pfx2, err := parseAddrRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid addr range2 %s, error %v", segs[0], err) + } + if af2 != af { + return fmt.Errorf("address family mismatch in hash:net,port,net,port member") + } + if startIP2 != nil { + param.GetAddrRange2().SetMinAddr(startIP2) + } + if endIP2 != nil { + param.GetAddrRange2().SetMaxAddr(endIP2) + } else { + param.GetAddrRange2().SetMaxAddr(startIP2) + } + param.SetCidr2(pfx2) + + segs = segs[1:] + startPort2, endPort2, proto2, err := parsePortRange(segs[0]) + if err != nil { + return fmt.Errorf("invalid port range2 %s, err %v", segs[0], err) + } + if proto2 != proto { + return fmt.Errorf("protocol mismatch in hash:net,port,net,port member") + } + param.GetAddrRange2().SetMinPort(startPort2) + if endPort2 > 0 { + param.GetAddrRange2().SetMaxPort(endPort2) + } else { + param.GetAddrRange2().SetMaxPort(startPort2) + } + + return nil +} + +func (o *IPSetHashNetPortNetPort) ModelEntry(af uint8, member *IPSetMember) (*models.IpsetMember, error) { + model := &models.IpsetMember{} + + proto := protoString(member.GetProto()) + entry := fmt.Sprintf("%s/%d,%s:%d,%s/%d,%s:%d", + member.GetAddr(af).String(), + member.GetCidr(), + proto, member.GetPort(), + member.GetAddr2(af).String(), + member.GetCidr2(), + proto, member.GetPort2()) + model.Entry = &entry + + return model, nil +} + +func (o *IPSetHashNetPortNetPort) CheckParam(param *IPSetParam) error { + // nothing to do + return nil +} + +var ipsetTypes = map[models.IpsetType]IPSetType{ + models.IpsetTypeBitmapIP: &IPSetBitmapIP{}, + models.IpsetTypeBitmapIPMac: &IPSetBitmapIPMac{}, + models.IpsetTypeBitmapPort: &IPSetBitmapPort{}, + models.IpsetTypeHashIP: &IPSetHashIP{}, + models.IpsetTypeHashNet: &IPSetHashNet{}, + models.IpsetTypeHashIPPort: &IPSetHashIPPort{}, + models.IpsetTypeHashNetPort: &IPSetHashNetPort{}, + models.IpsetTypeHashNetPortIface: &IPSetHashNetPortIface{}, + models.IpsetTypeHashIPPortIP: &IPSetHashIPPortIP{}, + models.IpsetTypeHashIPPortNet: &IPSetHashIPPortNet{}, + models.IpsetTypeHashNetPortNet: &IPSetHashNetPortNet{}, + models.IpsetTypeHashNetPortNetPort: &IPSetHashNetPortNetPort{}, +} + +func IPSetTypeGet(kind models.IpsetType) IPSetType { + return ipsetTypes[kind] +} + +func afCode(family string) uint8 { + switch strings.ToLower(family) { + case "": + return syscall.AF_UNSPEC + case "ipv4": + return syscall.AF_INET + case "ipv6": + return syscall.AF_INET6 + default: + return syscall.AF_MAX + } +} + +func afString(af uint8) string { + switch af { + case syscall.AF_INET: + return "ipv4" + case syscall.AF_INET6: + return "ipv6" + default: + return "not-supported" + } +} + +func protoString(proto uint8) string { + switch proto { + case syscall.IPPROTO_TCP: + return "tcp" + case syscall.IPPROTO_UDP: + return "udp" + case syscall.IPPROTO_ICMP: + return "icmp" + case syscall.IPPROTO_ICMPV6: + return "icmp6" + } + return "unspec" +} + +func ip4ToUint32(ip net.IP) uint32 { + ip4 := ip.To4() + if ip4 == nil { + return 0 + } + return binary.BigEndian.Uint32(ip4) +} + +// Parse IP range +// Format: +// +// { IPv4 | IPv4-IPv4 | IPv4/pfx4 | IPv6 | IPv6/pfx6 } +// +// Example: +// - 192.168.1.0/24 +// - 192.168.88.100-120 +// - 2001::/112 +func parseAddrRange(ar string) (startIP, endIP net.IP, af, cidr uint8, err error) { + if strings.Contains(ar, ":") { + af = syscall.AF_INET6 + } else { + af = syscall.AF_INET + } + + if af == syscall.AF_INET { + if strings.Contains(ar, "-") { + parts := strings.Split(ar, "-") + if len(parts) != 2 { + err = fmt.Errorf("invalid IPv4 range format %q", ar) + return + } + startIP = net.ParseIP(parts[0]).To4() + endIP = net.ParseIP(parts[1]).To4() + if startIP == nil || endIP == nil { + err = fmt.Errorf("invalid IPv4 address %q", ar) + return + } + if ip4ToUint32(startIP) > ip4ToUint32(endIP) { + err = fmt.Errorf("invalid IPv4 range %q", ar) + return + } + } else if strings.Contains(ar, "/") { + ip, ipNet, err2 := net.ParseCIDR(ar) + if err2 != nil { + err = fmt.Errorf("invalid IPv4 CIDR format: %v", err2) + return + } + startIP = ip.To4() + pfx, _ := ipNet.Mask.Size() + cidr = uint8(pfx) + } else { + if startIP = net.ParseIP(ar); startIP != nil { + startIP = startIP.To4() + } + if startIP == nil { + err = fmt.Errorf("unsupported IPv4 format") + return + } + } + } else { // syscall.AF_INET6 + if strings.Contains(ar, "/") { + ip, ipNet, err2 := net.ParseCIDR(ar) + if err2 != nil { + err = fmt.Errorf("invalid IPv6 CIDR format: %v", err2) + return + } + startIP = ip.To16() + pfx, _ := ipNet.Mask.Size() + cidr = uint8(pfx) + } else { + startIP = net.ParseIP(ar) + if startIP == nil { + err = fmt.Errorf("unsupported IPv6 format") + return + } + } + } + + return +} + +// Format: +// +// PROTO:PORT[-PORT] +// PROTO := tcp | udp | icmp | icmp6 +// PORT := NUM(0-65535) +// +// Example: +// +// tcp:8080-8082 +func parsePortRange(pr string) (port1, port2 uint16, proto uint8, err error) { + parts := strings.Split(pr, ":") + if len(parts) > 2 { + err = fmt.Errorf("too many segments in %q", pr) + return + } + + if len(parts) > 1 { + protoStr := strings.ToLower(parts[0]) + switch protoStr { + case "tcp": + proto = syscall.IPPROTO_TCP + case "udp": + proto = syscall.IPPROTO_UDP + case "icmp": + proto = syscall.IPPROTO_ICMP + case "icmp6": + proto = syscall.IPPROTO_ICMPV6 + default: + err = fmt.Errorf("invalid protocol %q", protoStr) + return + } + parts = parts[1:] + } + + portRange := parts[0] + parts = strings.Split(portRange, "-") + if len(parts) > 2 { + err = fmt.Errorf("too many segments in port range %q", portRange) + return + } + + _port1, err := strconv.ParseUint(parts[0], 10, 16) + if err != nil { + err = fmt.Errorf("invalid port number %q", parts[0]) + return + } + port1 = uint16(_port1) + + if len(parts) > 1 { + var _port2 uint64 + _port2, err = strconv.ParseUint(parts[1], 10, 16) + if err != nil { + err = fmt.Errorf("invalid port number %q", parts[1]) + return + } + port2 = uint16(_port2) + } + return +} + +func (o *IPSetParam) Build(model *models.IpsetInfo) error { + if len(model.Entries) > 0 && model.CreationOptions != nil { + return fmt.Errorf("Entries and CreationOptions cannot both set") + } + if len(model.Entries) > 1 { + return fmt.Errorf("More than 1 entry set for IPSetParam") + } + + o.SetName(*model.Name) + o.SetKind(string(*model.Type)) + + if model.CreationOptions != nil { + options := model.CreationOptions + if options.Comment != nil { + o.SetCommentFlag(*options.Comment) + } + o.SetMaxElem(options.HashMaxElem) + o.SetHashSize(options.HashSize) + af := afCode(options.Family) + if af == syscall.AF_MAX { + return fmt.Errorf("Unsupported address family %q", options.Family) + } + o.SetAf(af) + if len(options.Range) > 0 { + if strings.ContainsAny(options.Range, ".:") && unicode.Is(unicode.ASCII_Hex_Digit, + rune(options.Range[0])) { // IPv4 or IPv6 + startIP, endIP, af2, pfx, err := parseAddrRange(options.Range) + if err != nil { + return err + } + if af2 != af { + if af == syscall.AF_UNSPEC { + o.SetAf(af2) + } else { + return fmt.Errorf("Address family mismatch") + } + } + o.GetAddrRange().SetMinAddr(startIP) + if endIP != nil { + o.GetAddrRange().SetMaxAddr(endIP) + } + if pfx > 0 { + o.SetCidr(pfx) + } + } else { // Port + startPort, endPort, proto, err := parsePortRange(options.Range) + if err != nil { + return err + } + o.GetAddrRange().SetMinPort(startPort) + if endPort != 0 { + o.GetAddrRange().SetMaxPort(endPort) + } else { + o.GetAddrRange().SetMaxPort(startPort) + } + o.SetProto(proto) + } + } + } + + if len(model.Entries) > 0 { + return o.BuildMember(model.Entries[0]) + } + return nil +} + +func (o *IPSetParamArray) Build(opcode uint16, model *models.IpsetInfo) error { + if opcode == IPSET_OP_FLUSH { + param := new(IPSetParam) + param.SetOpcode(opcode) + param.SetName(*model.Name) + param.SetKind(string(*model.Type)) + *o = append(*o, *param) + return nil + } + + if len(model.Entries) < 1 { + return fmt.Errorf("No Entries found in IpsetInfo model") + } + if model.CreationOptions != nil { + return fmt.Errorf("CreationOptions supplied with multiple Entries") + } + + for _, entry := range model.Entries { + param := new(IPSetParam) + param.SetOpcode(opcode) + param.SetName(*model.Name) + param.SetKind(string(*model.Type)) + err := param.BuildMember(entry) + if err != nil { + return fmt.Errorf("Parse ipset member %v failed: %v", entry.Entry, err) + } + *o = append(*o, *param) + } + return nil +} + +// o.kind must be filled before calling BuildMember +func (o *IPSetParam) BuildMember(model *models.IpsetMember) error { + kind := o.getKind() + setType := IPSetTypeGet(models.IpsetType(kind)) + if setType == nil { + return fmt.Errorf("Unsupported ipset type %q", kind) + } + + if model.Entry == nil { + return fmt.Errorf("Empty ipset member entry") + } + + o.SetComment(model.Comment) + if model.Options != nil { + if model.Options.Force != nil && *model.Options.Force { + o.AddFlag(IPSET_F_FORCE) + } else { + o.DelFlag(IPSET_F_FORCE) + } + if model.Options.NoMatch != nil { + o.SetNomatch(*model.Options.NoMatch) + } + } + + return setType.ParseEntry(*model.Entry, o) +} + +func (o *IPSetParam) Check() error { + if o.opcode >= IPSET_OP_MAX { + return fmt.Errorf("Invalid ipset opcode %v", o.opcode) + } else if o.opcode == IPSET_OP_LIST { + return nil + } else if o.opcode == IPSET_OP_TEST { + if o.cidr > 0 || o.cidr2 > 0 { + return fmt.Errorf("Cidr set in IPSET_OP_TEST (IsIn)") + } + } + + kind := o.getKind() + setType := IPSetTypeGet(models.IpsetType(kind)) + if setType == nil { + return fmt.Errorf("Unsupported ipset type %q", kind) + } + + startIP1, endIP1, startPort1, endPort1 := o.addrRange.Decode(o.af) + startIP2, endIP2, startPort2, endPort2 := o.addrRange2.Decode(o.af) + if o.af == syscall.AF_INET6 { + if !endIP1.Equal(net.IPv6zero) && !endIP1.Equal(startIP1) { + return fmt.Errorf("IPv6 range is not supported") + } + if !endIP2.Equal(net.IPv6zero) && !endIP2.Equal(startIP2) { + return fmt.Errorf("IPv6 range is not supported") + } + } else if o.af == syscall.AF_INET { + start, end := ip4ToUint32(startIP1), ip4ToUint32(endIP1) + if end != 0 && start > end { + return fmt.Errorf("Invalid IPv4 range: %v-%v", startIP1, endIP1) + } + start, end = ip4ToUint32(startIP2), ip4ToUint32(endIP2) + if end != 0 && start > end { + return fmt.Errorf("Invalid IPv4 range: %v-%v", startIP2, endIP2) + } + } + + if endPort1 > 0 && startPort1 > endPort1 { + return fmt.Errorf("Invalid port range: %d-%d", startPort1, endPort1) + } + + if endPort2 > 0 && startPort2 > endPort2 { + return fmt.Errorf("Invalid port range: %d-%d", startPort2, endPort2) + } + + return setType.CheckParam(o) +} + +func (o *IPSetParamArray) Check() error { + for _, param := range *o { + if err := param.Check(); err != nil { + return err + } + } + return nil +} + +func (o *IPSetMember) Model(af uint8, kind models.IpsetType) (*models.IpsetMember, error) { + setType := IPSetTypeGet(kind) + if setType == nil { + return nil, fmt.Errorf("Unsupported ipset type %q", kind) + } + + model, err := setType.ModelEntry(af, o) + if err != nil { + return nil, err + } + + model.Comment = o.GetComment() + nomatch := o.GetNoMatch() + if nomatch { + model.Options = &models.IpsetOption{} + model.Options.NoMatch = &nomatch + } + return model, nil +} + +func (o *IPSetInfo) Model() (*models.IpsetInfo, error) { + model := new(models.IpsetInfo) + model.Name = new(string) + model.Type = new(models.IpsetType) + model.CreationOptions = new(models.IpsetCreationOption) + + *model.Name = o.GetName() + *model.Type = models.IpsetType(o.GetKind()) + model.Opcode = IPSET_OP_LIST + + af := o.GetAf() + cidr := o.GetCidr() + withIP := false + + copts := model.CreationOptions + if o.GetComment() { + copts.Comment = new(bool) + *copts.Comment = true + } + copts.Family = afString(af) + if strings.HasPrefix(string(*model.Type), "hash:") { + copts.HashMaxElem = o.GetHashMaxElem() + copts.HashSize = o.GetHashSize() + } else if strings.HasPrefix(string(*model.Type), "bitmap:") { + copts.Range = "" + startIP, endIP, startPort, endPort := o.GetAddrRange() + if cidr > 0 { + copts.Range += fmt.Sprintf("%s/%d", startIP, cidr) + withIP = true + } else if af == syscall.AF_INET { + startIPNum, endIPNum := ip4ToUint32(startIP), ip4ToUint32(endIP) + if endIPNum > startIPNum { + copts.Range += fmt.Sprintf("%s-%s", startIP.String(), endIP.String()) + withIP = true + } + } + if endPort > startPort { + if withIP { + copts.Range += ":" + } + copts.Range += fmt.Sprintf("%d-%d", startPort, endPort) + } + } + + for _, member := range o.GetMembers() { + memberModel, err := member.Model(af, *model.Type) + if err != nil { + return nil, err + } + model.Entries = append(model.Entries, memberModel) + } + + return model, nil +} + +func (o *IPSetInfoArray) Model() (*models.IpsetInfoArray, error) { + model := new(models.IpsetInfoArray) + for _, info := range o.GetIPSetInfos() { + infoModel, err := info.Model() + if err != nil { + return nil, err + } + model.Infos = append(model.Infos, infoModel) + } + model.Count = int32(len(model.Infos)) + + return model, nil +} From 140c152651f9c5e7744a39ad5cd606d72a947f94 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Tue, 27 Aug 2024 19:06:08 +0800 Subject: [PATCH 54/63] dpvs-agent: add ipset test script Signed-off-by: ywc689 --- test/ipset/dpvs-agent.sh | 158 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100755 test/ipset/dpvs-agent.sh diff --git a/test/ipset/dpvs-agent.sh b/test/ipset/dpvs-agent.sh new file mode 100755 index 000000000..8a22a82d0 --- /dev/null +++ b/test/ipset/dpvs-agent.sh @@ -0,0 +1,158 @@ +#!/bin/env sh +# + +## <> +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"bitmap:ip,mac","Name":"ttt","CreationOptions":{"Family":"ipv4","Comment":true,"Range":"192.168.88.0/24"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"bitmap:ip,mac","Name":"ttt","Entries":[{"Entry":"192.168.88.1,AA:bb:CC:11:22:33"},{"Entry":"192.168.88.100","Comment":"no mac","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"bitmap:ip,mac","Member":{"Entry":"192.168.88.1,AA:bb:CC:11:22:33"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"bitmap:ip,mac","Name":"ttt","Entries":[{"Entry":"192.168.88.100,AA:bb:CC:11:22:33"}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"bitmap:ip,mac","Name":"ttt","Entries":[{"Entry":"192.168.88.100","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +## <> + +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"bitmap:port","Name":"ttt","CreationOptions":{"Comment":true,"Range":"10000-20000"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"bitmap:port","Name":"ttt","Entries":[{"Entry":"tcp:10000-10002"},{"Entry":"tcp:10888","Comment":"single","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"bitmap:port","Member":{"Entry":"tcp:12222"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"bitmap:port","Name":"ttt","Entries":[{"Entry":"tcp:10000-10008"}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"bitmap:port","Name":"ttt","Entries":[{"Entry":"tcp:10003","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +## <> +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:ip","Name":"ttt","CreationOptions":{"Comment":true,"Family":"ipv4","HashSize": 128,"HashMaxElem": 10001}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip","Name":"ttt","Entries":[{"Entry":"192.168.88.100/30","Comment":"a cidr"},{"Entry":"10.64.68.1-10.64.68.3","Comment":"a range","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:ip","Member":{"Entry":"192.168.88.100"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip","Name":"ttt","Entries":[{"Entry":"192.168.88.100/30","Comment":"a cidr"},{"Entry":"10.64.68.10-10.64.68.14","Comment":"a range","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip","Name":"ttt","Entries":[{"Entry":"192.168.88.100-192.168.88.128","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:ip","Name":"ttt","CreationOptions":{"Comment":true,"Family":"ipv6","HashSize": 256,"HashMaxElem": 20001}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip","Name":"ttt","Entries":[{"Entry":"2001::B","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:ip","Member":{"Entry":"2001::a"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip","Name":"ttt","Entries":[{"Entry":"2001::b","Comment":"replace","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip","Name":"ttt","Entries":[{"Entry":"2001::a","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +## <> +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:net","Name":"ttt","CreationOptions":{"Comment":true,"Family":"ipv4"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net","Name":"ttt","Entries":[{"Entry":"192.168.88.128/26","Comment":"net1"},{"Entry":"10.64.0.10-10.64.0.20","Comment":"a net range","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:net","Member":{"Entry":"192.168.88.100"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net","Name":"ttt","Entries":[{"Entry":"192.168.88.128/26","Comment":"net1"},{"Entry":"192.168.88.164/30","Comment":"net1 nomatch","Options":{"NoMatch":true}},{"Entry":"10.64.0.10-10.64.0.20","Comment":"a net range","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net","Name":"ttt","Entries":[{"Entry":"192.168.88.192/28","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:net","Name":"ttt","CreationOptions":{"Comment":true,"Family":"ipv6"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net","Name":"ttt","Entries":[{"Entry":"2001::/64","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:net","Member":{"Entry":"2001::66"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net","Name":"ttt","Entries":[{"Entry":"2002::/64","Comment":"replace","Options":{"Force":false}},{"Entry":"2002::ff:0:0/96","Comment":"net1 nomatch","Options":{"NoMatch":true}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net","Name":"ttt","Entries":[{"Entry":"2002::/64","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +## <> +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:net,port","Name":"ttt","CreationOptions":{"Comment":true,"Family":"ipv4"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port","Name":"ttt","Entries":[{"Entry":"192.168.88.0/24,tcp:8080","Comment":"net cidr"},{"Entry":"10.64.0.10-10.64.0.20","Comment":"net range","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:net,port","Member":{"Entry":"192.168.88.100,tcp:8080"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port","Name":"ttt","Entries":[{"Entry":"192.168.88.128/26,udp:80-82","Comment":"net1"},{"Entry":"192.168.88.164/30,udp:80","Comment":"net1 nomatch","Options":{"NoMatch":true}},{"Entry":"10.64.0.10-10.64.0.20,tcp:6600","Comment":"a net range","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port","Name":"ttt","Entries":[{"Entry":"192.168.88.128/26,udp:81","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:net,port","Name":"ttt","CreationOptions":{"Comment":true,"Family":"ipv6"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port","Name":"ttt","Entries":[{"Entry":"2001::/64,tcp:8080-8083","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:net,port","Member":{"Entry":"2001::66,tcp:8082"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port","Name":"ttt","Entries":[{"Entry":"2002::/64,udp:80-83","Comment":"replace","Options":{"Force":false}},{"Entry":"2002::ff:0:0/96,udp:80","Comment":"net1 nomatch","Options":{"NoMatch":true}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port","Name":"ttt","Entries":[{"Entry":"2002::/64,udp:82","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +## <> +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:net,port,iface","Name":"ttt","CreationOptions":{"Comment":true,"Family":"ipv4"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,iface","Name":"ttt","Entries":[{"Entry":"192.168.88.0/24,tcp:8080,dpdk0","Comment":"net cidr"},{"Entry":"10.64.0.10-10.64.0.20,tcp:80-82,dpdk0","Comment":"net range","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:net,port,iface","Member":{"Entry":"192.168.88.100,tcp:8080,dpdk0"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,iface","Name":"ttt","Entries":[{"Entry":"192.168.88.0/24,tcp:80-82,dpdk0","Comment":"net cidr"},{"Entry":"10.64.0.10-10.64.0.20,tcp:8080,dpdk0","Comment":"net range","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,iface","Name":"ttt","Entries":[{"Entry":"192.168.88.0/24,tcp:81,dpdk0","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:net,port,iface","Name":"ttt","CreationOptions":{"Comment":false,"Family":"ipv6"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,iface","Name":"ttt","Entries":[{"Entry":"2001::/64,tcp:8080-8083,dpdk0","Options":{"Force":false}},{"Entry":"2002::/64,udp:6600,dpdk0","Comment":"xxxxx","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:net,port,iface","Member":{"Entry":"2001::66,tcp:8082,dpdk0"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,iface","Name":"ttt","Entries":[{"Entry":"2002::/64,udp:80-83,dpdk0","Comment":"zzzzz","Options":{"Force":false}},{"Entry":"2002::ff:0:0/96,udp:80,dpdk0","Comment":"net1 nomatch","Options":{"NoMatch":true}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,iface","Name":"ttt","Entries":[{"Entry":"2002::/64,udp:82,dpdk0","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +## <> +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Name":"ttt","CreationOptions":{"Comment":true,"Family":"ipv4"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Name":"ttt","Entries":[{"Entry":"192.168.1.16/31,tcp:8080-8081,192.168.2.100-192.168.2.102","Comment":"net-port-range"},{"Entry":"10.64.0.10-10.64.0.20,udp:6600,112.112.112.112","Comment":"udp","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Member":{"Entry":"192.168.1.17,tcp:8080,192.168.2.100"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Name":"ttt","Entries":[{"Entry":"192.168.1.16/31,tcp:8080-8081,192.168.2.100-192.168.2.102","Comment":"net-port-range"},{"Entry":"10.64.88.0,udp:6600,112.112.112.112","Comment":"udp","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Name":"ttt","Entries":[{"Entry":"192.1681.16/31,tcp:8081,192.168.2.101","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Name":"ttt","CreationOptions":{"Comment":false,"Family":"ipv6"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Name":"ttt","Entries":[{"Entry":"2001::4444,tcp:8080-8083,2002::7777","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Member":{"Entry":"2001::4444,tcp:8082,2002::7777"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Name":"ttt","Entries":[{"Entry":"2002::aaaa,udp:80-83,2001::6666","Comment":"xxxxx","Options":{"Force":false}},{"Entry":"2002::ff:1,tcp:80,2001::ee:2","Comment":"net1 nomatch","Options":{"NoMatch":true}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Name":"ttt","Entries":[{"Entry":"2002::aaaa,udp:82,2001::6666","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,ip","Name":"ttt","Entries":[{"Entry":"2002::aaaa,udp:80-83,2001::6666","Comment":"xxxxx","Options":{"Force":true}},{"Entry":"2002::ff:1,tcp:80,2001::ee:2","Comment":"net1 nomatch","Options":{"NoMatch":true}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +## <> +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,net","Name":"ttt"}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,net","Name":"ttt","Entries":[{"Entry":"192.168.1.5-192.168.1.6,tcp:8081-8082,192.168.88.0/24"},{"Entry":"10.64.100.100/30,udp:6600,10.64.200.0/24","Comment":"udp","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,net","Member":{"Entry":"192.168.1.6,tcp:8081,192.168.88.111"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,net","Name":"ttt","Entries":[{"Entry":"192.168.1.4/30,80,192.168.88.0/24"},{"Entry":"10.64.100.100/30,udp:6688,10.64.100.0/24","Comment":"udp","Options":{"Force":false}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,net","Name":"ttt","Entries":[{"Entry":"192.168.1.6/31,80,192.168.88.0/24"}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,net","Name":"ttt","CreationOptions":{"Comment":true,"Family":"ipv6","HashSize":64,"HashMaxElem":20000}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,net","Name":"ttt","Entries":[{"Entry":"2001::1,tcp:8080-8083,2002::0/64"},{"Entry":"2001::1,tcp:8080,2002::FFFF:0/112","Options":{"NoMatch":true}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,net","Member":{"Entry":"2001::1,tcp:8082,2002::FFFF:1"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,net","Name":"ttt","Entries":[{"Entry":"2001::1,tcp:8080-8083,2002::0/64"},{"Entry":"2001::1,tcp:8080,2001:FFFF::0/80","Comment":"replaced","Options":{"NoMatch":true}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:ip,port,net","Name":"ttt","Entries":[{"Entry":"2001::1,tcp:8080,2002::0/64"}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +## <> +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net","Name":"ttt"}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net","Name":"ttt","Entries":[{"Entry":"192.168.1.0/24,tcp:8080-8083,192.168.2.0/24"},{"Entry":"10.64.96.0/21,udp:6600,10.132.80.0/21","Comment":"udp","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net","Member":{"Entry":"192.168.1.4,tcp:8080,192.168.2.254"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net","Name":"ttt","Entries":[{"Entry":"192.168.1.0/24,tcp:8080-8083,192.168.2.0/24"},{"Entry":"10.64.96.0/21,udp:6600,10.132.80.0/21","Comment":"udp","Options":{"Force":false}},{"Entry":"10.64.97.0/24,udp:6600,10.132.82.0/24","Comment":"udp","Options":{"NoMatch":true}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net","Name":"ttt","Entries":[{"Entry":"192.168.1.0/24,tcp:8082,192.168.2.0/24"}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net","Name":"ttt","CreationOptions":{"Comment":true,"Family":"ipv6"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net","Name":"ttt","Entries":[{"Entry":"2001::/64,tcp:8080-8083,2002::0/64"},{"Entry":"2001::/120,tcp:8080,2002::FFFF:0/112","Options":{"NoMatch":true}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net","Member":{"Entry":"2001::1,tcp:8082,2002::FFFF:1"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net","Name":"ttt","Entries":[{"Entry":"2001::/64,tcp:8080-8083,2002::0/64"},{"Entry":"2001::/112,tcp:8080,2002::FFFF:0/112","Comment":"replaced","Options":{"NoMatch":true}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net","Name":"ttt","Entries":[{"Entry":"2001::/64,tcp:8081,2002::/64"}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +## <> +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net,port","Name":"ttt","CreationOptions":{"Comment":true}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net,port","Name":"ttt","Entries":[{"Entry":"192.168.1.0/24,tcp:8080-8083,192.168.2.0/24,tcp:12345"},{"Entry":"10.64.96.0/21,udp:53,10.132.80.0/21,udp:6600-6601","Comment":"udp","Options":{"Force":false}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net,port","Member":{"Entry":"192.168.1.4,tcp:8080,192.168.2.254,tcp:12345"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net,port","Name":"ttt","Entries":[{"Entry":"192.168.1.0/24,tcp:8080-8083,192.168.2.0/24,tcp:12345"},{"Entry":"10.64.96.0/21,udp:53,10.132.80.0/21,udp:6600-6601","Comment":"udp","Options":{"Force":false}},{"Entry":"10.64.96.0/24,udp:53,10.132.80.86/24,udp:6601","Comment":"add exceptions","Options":{"Nomatch":true}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net,port","Name":"ttt","Entries":[{"Entry":"10.64.96.0/21,udp:53,10.132.80.0/21,udp:6600-6601"}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt + +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net,port","Name":"ttt","CreationOptions":{"Family":"ipv6"}}' +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net,port","Name":"ttt","Entries":[{"Entry":"2001::/64,tcp:8080-8083,2002::0/64,tcp:80","Options":{"Force":true}},{"Entry":"2001::/120,tcp:8080,2002::FFFF:0/112,tcp:80","Options":{"NoMatch":true,"Force":true}}]}' +curl -X GET http://127.0.0.1:8866/v2/ipset/ttt | jq | more +curl -X POST http://127.0.0.1:8866/v2/ipset/ttt/cell -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net,port","Member":{"Entry":"2001::1,tcp:8082,2002::FFFF:1,tcp:80"}}' +curl -X PUT http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net,port","Name":"ttt","Entries":[{"Entry":"2001::/64,tcp:8080-8083,2002::0/64,tcp:80"},{"Entry":"2001::/112,tcp:8080,2002::FFFF:0/112,tcp:80","Comment":"replaced","Options":{"NoMatch":true}}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt/member -H "Content-Type: application/json" -d '{"Type":"hash:net,port,net,port","Name":"ttt","Entries":[{"Entry":"2001::/64,tcp:8082,2002::/64,tcp:80"}]}' +curl -X DELETE http://127.0.0.1:8866/v2/ipset/ttt From 166b97767afe1daad30c84b0d8847e2331a8042a Mon Sep 17 00:00:00 2001 From: ywc689 Date: Mon, 2 Sep 2024 14:16:05 +0800 Subject: [PATCH 55/63] Fix segmentation fault problem when running on machines whose cpu number is over DPVS_MAX_LCORE. Fixed issue #991. Signed-off-by: ywc689 --- src/inetaddr.c | 8 +++++--- src/ipset/ipset_core.c | 9 ++++++++- src/ipv6/route6.c | 9 ++++++++- src/ipvs/ip_vs_blklst.c | 18 ++++++++++++++---- src/ipvs/ip_vs_conn.c | 15 ++++++++++++--- src/ipvs/ip_vs_whtlst.c | 18 ++++++++++++++---- src/route.c | 13 +++++++++++-- src/scheduler.c | 3 +++ src/timer.c | 14 ++++++++++++-- 9 files changed, 87 insertions(+), 20 deletions(-) diff --git a/src/inetaddr.c b/src/inetaddr.c index 077f9d844..af19d6fcc 100644 --- a/src/inetaddr.c +++ b/src/inetaddr.c @@ -238,10 +238,12 @@ static int __idev_add_mcast_init(void *args) struct inet_device *idev; union inet_addr all_nodes, all_routers; struct rte_ether_addr eaddr_nodes, eaddr_routers; - bool is_master = (rte_lcore_id() == g_master_lcore_id); - + lcoreid_t cid = rte_lcore_id(); + bool is_master = (cid == g_master_lcore_id); struct netif_port *dev = (struct netif_port *) args; + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; idev = dev_get_idev(dev); memset(&eaddr_nodes, 0, sizeof(eaddr_nodes)); @@ -1952,7 +1954,7 @@ static int ifa_sockopt_agent_get(sockoptid_t opt, const void *conf, size_t size, struct inet_device *idev = NULL; struct inet_addr_front *array = NULL; const struct inet_addr_entry *entry = conf; - int len; + int len = 0; int err; if (entry->af != AF_INET && entry->af != AF_INET6 && entry->af != AF_UNSPEC) { diff --git a/src/ipset/ipset_core.c b/src/ipset/ipset_core.c index 2d752be41..85d4325ff 100644 --- a/src/ipset/ipset_core.c +++ b/src/ipset/ipset_core.c @@ -227,6 +227,9 @@ ipset_flush_lcore(void *arg) int i; struct ipset *set; + if (rte_lcore_id() >= DPVS_MAX_LCORE) + return EDPVS_OK; + for (i = 0; i < IPSETS_TBL_SIZE; i++) { list_for_each_entry(set, &this_ipsets_tbl[i], list) set->type->destroy(set); @@ -244,8 +247,12 @@ static int ipset_lcore_init(void *arg) { int i; + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; - if (!rte_lcore_is_enabled(rte_lcore_id())) + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; this_ipsets_tbl = rte_zmalloc(NULL, diff --git a/src/ipv6/route6.c b/src/ipv6/route6.c index 0c3d06d79..311e9f64c 100644 --- a/src/ipv6/route6.c +++ b/src/ipv6/route6.c @@ -135,10 +135,14 @@ static int rt6_setup_lcore(void *arg) int err; bool global; struct timeval tv; + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; tv.tv_sec = g_rt6_recycle_time, tv.tv_usec = 0, - global = (rte_lcore_id() == rte_get_main_lcore()); + global = (cid == rte_get_main_lcore()); INIT_LIST_HEAD(&this_rt6_dustbin.routes); err = dpvs_timer_sched_period(&this_rt6_dustbin.tm, &tv, rt6_recycle, NULL, global); @@ -152,6 +156,9 @@ static int rt6_destroy_lcore(void *arg) { struct route6 *rt6, *next; + if (rte_lcore_id() >= DPVS_MAX_LCORE) + return EDPVS_OK; + list_for_each_entry_safe(rt6, next, &this_rt6_dustbin.routes, hnode) { if (rte_atomic32_read(&rt6->refcnt) <= 1) { /* need judge refcnt here? */ list_del(&rt6->hnode); diff --git a/src/ipvs/ip_vs_blklst.c b/src/ipvs/ip_vs_blklst.c index 25491a076..a608cd0c5 100644 --- a/src/ipvs/ip_vs_blklst.c +++ b/src/ipvs/ip_vs_blklst.c @@ -459,15 +459,20 @@ static struct dpvs_sockopts blklst_sockopts = { static int blklst_lcore_init(void *args) { int i; + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; - if (!rte_lcore_is_enabled(rte_lcore_id())) + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; this_num_blklsts = 0; this_num_blklsts_ipset = 0; - this_blklst_tab = rte_malloc(NULL, sizeof(struct list_head) * - DPVS_BLKLST_TAB_SIZE, RTE_CACHE_LINE_SIZE); + this_blklst_tab = rte_malloc(NULL, + sizeof(struct list_head) * DPVS_BLKLST_TAB_SIZE, + RTE_CACHE_LINE_SIZE); if (!this_blklst_tab) return EDPVS_NOMEM; for (i = 0; i < DPVS_BLKLST_TAB_SIZE; i++) @@ -487,7 +492,12 @@ static int blklst_lcore_init(void *args) static int blklst_lcore_term(void *args) { - if (!rte_lcore_is_enabled(rte_lcore_id())) + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; + + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; dp_vs_blklst_flush_all(); diff --git a/src/ipvs/ip_vs_conn.c b/src/ipvs/ip_vs_conn.c index 33268760c..4179e68ad 100644 --- a/src/ipvs/ip_vs_conn.c +++ b/src/ipvs/ip_vs_conn.c @@ -1192,11 +1192,15 @@ static void dp_vs_conn_put_nolock(struct dp_vs_conn *conn) static int conn_init_lcore(void *arg) { int i; + lcoreid_t cid = rte_lcore_id(); - if (!rte_lcore_is_enabled(rte_lcore_id())) + if (cid >= DPVS_MAX_LCORE) + return EDPVS_IDLE; + + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; - if (!netif_lcore_is_fwd_worker(rte_lcore_id())) + if (!netif_lcore_is_fwd_worker(cid)) return EDPVS_IDLE; this_conn_tbl = rte_malloc(NULL, @@ -1218,7 +1222,12 @@ static int conn_init_lcore(void *arg) static int conn_term_lcore(void *arg) { - if (!rte_lcore_is_enabled(rte_lcore_id())) + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_IDLE; + + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; if (this_conn_tbl) { diff --git a/src/ipvs/ip_vs_whtlst.c b/src/ipvs/ip_vs_whtlst.c index 672077456..36023e6ca 100644 --- a/src/ipvs/ip_vs_whtlst.c +++ b/src/ipvs/ip_vs_whtlst.c @@ -522,15 +522,20 @@ static struct dpvs_sockopts whtlst_sockopts = { static int whtlst_lcore_init(void *args) { int i; + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; - if (!rte_lcore_is_enabled(rte_lcore_id())) + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; this_num_whtlsts = 0; this_num_whtlsts_ipset = 0; - this_whtlst_tab = rte_malloc(NULL, sizeof(struct list_head) * - DPVS_WHTLST_TAB_SIZE, RTE_CACHE_LINE_SIZE); + this_whtlst_tab = rte_malloc(NULL, + sizeof(struct list_head) * DPVS_WHTLST_TAB_SIZE, + RTE_CACHE_LINE_SIZE); if (!this_whtlst_tab) return EDPVS_NOMEM; for (i = 0; i < DPVS_WHTLST_TAB_SIZE; i++) @@ -550,7 +555,12 @@ static int whtlst_lcore_init(void *args) static int whtlst_lcore_term(void *args) { - if (!rte_lcore_is_enabled(rte_lcore_id())) + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; + + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; dp_vs_whtlst_flush_all(); diff --git a/src/route.c b/src/route.c index 00354f7c8..f48d82fa2 100644 --- a/src/route.c +++ b/src/route.c @@ -671,8 +671,12 @@ static struct dpvs_sockopts route_sockopts = { static int route_lcore_init(void *arg) { int i; + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; - if (!rte_lcore_is_enabled(rte_lcore_id())) + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; for (i = 0; i < LOCAL_ROUTE_TAB_SIZE; i++) @@ -684,7 +688,12 @@ static int route_lcore_init(void *arg) static int route_lcore_term(void *arg) { - if (!rte_lcore_is_enabled(rte_lcore_id())) + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; + + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; return route_lcore_flush(); diff --git a/src/scheduler.c b/src/scheduler.c index d446f378b..338cfa238 100644 --- a/src/scheduler.c +++ b/src/scheduler.c @@ -188,6 +188,9 @@ static int dpvs_job_loop(void *arg) thres_time = BIG_LOOP_THRESH_MASTER; #endif + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; + /* skip irrelative job loops */ if (role == LCORE_ROLE_MAX) return EDPVS_INVAL; diff --git a/src/timer.c b/src/timer.c index dd3485375..b3ae9d505 100644 --- a/src/timer.c +++ b/src/timer.c @@ -412,7 +412,12 @@ static int timer_term_schedler(struct timer_scheduler *sched) static int timer_lcore_init(void *arg) { - if (!rte_lcore_is_enabled(rte_lcore_id())) + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; + + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; return timer_init_schedler(&RTE_PER_LCORE(timer_sched), rte_lcore_id()); @@ -420,7 +425,12 @@ static int timer_lcore_init(void *arg) static int timer_lcore_term(void *arg) { - if (!rte_lcore_is_enabled(rte_lcore_id())) + lcoreid_t cid = rte_lcore_id(); + + if (cid >= DPVS_MAX_LCORE) + return EDPVS_OK; + + if (!rte_lcore_is_enabled(cid)) return EDPVS_DISABLED; return timer_term_schedler(&RTE_PER_LCORE(timer_sched)); From 257d89fdddc5789cf121b2caf5269b33618920c1 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Thu, 5 Sep 2024 17:35:52 +0800 Subject: [PATCH 56/63] netif: refactor netif_rte_port_alloc with netif_alloc Signed-off-by: ywc689 --- include/netif.h | 4 +- src/ip_tunnel.c | 3 +- src/netif.c | 134 ++++++++++++++++++------------------------------ src/vlan.c | 4 +- 4 files changed, 55 insertions(+), 90 deletions(-) diff --git a/include/netif.h b/include/netif.h index af3adeb7a..274f4b3d4 100644 --- a/include/netif.h +++ b/include/netif.h @@ -72,7 +72,7 @@ enum { #define NETIF_ALIGN 32 -#define NETIF_PORT_ID_INVALID 0xFF +#define NETIF_PORT_ID_INVALID NETIF_MAX_PORTS #define NETIF_PORT_ID_ALL NETIF_PORT_ID_INVALID #define NETIF_LCORE_ID_INVALID 0xFF @@ -283,7 +283,7 @@ int netif_get_promisc(struct netif_port *dev, bool *promisc); int netif_get_allmulticast(struct netif_port *dev, bool *allmulticast); int netif_get_stats(struct netif_port *dev, struct rte_eth_stats *stats); int netif_get_xstats(struct netif_port *dev, netif_nic_xstats_get_t **xstats); -struct netif_port *netif_alloc(size_t priv_size, const char *namefmt, +struct netif_port *netif_alloc(portid_t id, size_t priv_size, const char *namefmt, unsigned int nrxq, unsigned int ntxq, void (*setup)(struct netif_port *)); portid_t netif_port_count(void); diff --git a/src/ip_tunnel.c b/src/ip_tunnel.c index 3e1e3483b..a0e66bbbf 100644 --- a/src/ip_tunnel.c +++ b/src/ip_tunnel.c @@ -171,7 +171,8 @@ static struct netif_port *tunnel_create(struct ip_tunnel_tab *tab, if (!strlen(params.ifname)) snprintf(params.ifname, IFNAMSIZ, "%s%%d", ops->kind); - dev = netif_alloc(ops->priv_size, params.ifname, 1, 1, ops->setup); + dev = netif_alloc(NETIF_PORT_ID_INVALID, ops->priv_size, params.ifname, + 1, 1, ops->setup); if (!dev) return NULL; diff --git a/src/netif.c b/src/netif.c index 3fd24aa0b..f0113c519 100644 --- a/src/netif.c +++ b/src/netif.c @@ -2766,8 +2766,6 @@ static inline void netif_lcore_cleanup(void) } } -/********************************************** kni *************************************************/ - /* always update bond port macaddr and its KNI macaddr together */ static int update_bond_macaddr(struct netif_port *port) { @@ -2799,6 +2797,8 @@ static inline void free_mbufs(struct rte_mbuf **pkts, unsigned num) } } +/********************************************** kni *************************************************/ + void kni_ingress(struct rte_mbuf *mbuf, struct netif_port *dev) { if (!kni_dev_exist(dev)) @@ -3224,7 +3224,7 @@ portid_t netif_port_count(void) return port_id_end; } -struct netif_port *netif_alloc(size_t priv_size, const char *namefmt, +struct netif_port *netif_alloc(portid_t id, size_t priv_size, const char *namefmt, unsigned int nrxq, unsigned int ntxq, void (*setup)(struct netif_port *)) { @@ -3247,13 +3247,17 @@ struct netif_port *netif_alloc(size_t priv_size, const char *namefmt, return NULL; } - dev->id = netif_port_id_alloc(); + if (id != NETIF_PORT_ID_INVALID && !netif_port_get(id)) + dev->id = id; + else + dev->id = netif_port_id_alloc(); if (strstr(namefmt, "%d")) snprintf(dev->name, sizeof(dev->name), namefmt, dev->id); else snprintf(dev->name, sizeof(dev->name), "%s", namefmt); + rte_rwlock_init(&dev->dev_lock); dev->socket = SOCKET_ID_ANY; dev->hw_header_len = sizeof(struct rte_ether_hdr); /* default */ @@ -3277,7 +3281,6 @@ struct netif_port *netif_alloc(size_t priv_size, const char *namefmt, if (dev->mtu == 0) dev->mtu = ETH_DATA_LEN; - rte_rwlock_init(&dev->dev_lock); netif_mc_init(dev); dev->in_ptr = rte_zmalloc(NULL, sizeof(struct inet_device), RTE_CACHE_LINE_SIZE); @@ -3474,79 +3477,6 @@ static inline void setup_dev_of_flags(struct netif_port *port) port->flag |= NETIF_PORT_FLAG_LLDP; } -/* TODO: refactor it with netif_alloc */ -static struct netif_port* netif_rte_port_alloc(portid_t id, int nrxq, - int ntxq, const struct rte_eth_conf *conf) -{ - int ii; - struct netif_port *port; - - port = rte_zmalloc("port", sizeof(struct netif_port) + - sizeof(union netif_bond), RTE_CACHE_LINE_SIZE); - if (!port) { - RTE_LOG(ERR, NETIF, "%s: no memory\n", __func__); - return NULL; - } - - port->id = id; - port->bond = (union netif_bond *)(port + 1); - if (is_physical_port(id)) { - port->type = PORT_TYPE_GENERAL; /* update later in netif_rte_port_alloc */ - port->netif_ops = &dpdk_netif_ops; - } else if (is_bond_port(id)) { - port->type = PORT_TYPE_BOND_MASTER; - port->netif_ops = &bond_netif_ops; - } else { - RTE_LOG(ERR, NETIF, "%s: invalid port id: %d\n", __func__, id); - rte_free(port); - return NULL; - } - - if (port_name_alloc(id, port->name, sizeof(port->name)) != EDPVS_OK) { - RTE_LOG(ERR, NETIF, "%s: fail to get port name for port%d\n", - __func__, id); - rte_free(port); - return NULL; - } - - port->nrxq = nrxq; // update after port_rx_queues_get(); - port->ntxq = ntxq; // update after port_tx_queues_get(); - port->socket = rte_eth_dev_socket_id(id); - port->hw_header_len = sizeof(struct rte_ether_hdr); - if (port->socket == SOCKET_ID_ANY) - port->socket = rte_socket_id(); - port->mbuf_pool = pktmbuf_pool[port->socket]; - rte_eth_macaddr_get((uint8_t)id, &port->addr); // bonding mac is zero here - rte_eth_dev_get_mtu((uint8_t)id, &port->mtu); - rte_eth_dev_info_get((uint8_t)id, &port->dev_info); - port->dev_conf = *conf; - rte_rwlock_init(&port->dev_lock); - netif_mc_init(port); - - setup_dev_of_flags(port); - - port->in_ptr = rte_zmalloc(NULL, sizeof(struct inet_device), RTE_CACHE_LINE_SIZE); - if (!port->in_ptr) { - RTE_LOG(ERR, NETIF, "%s: no memory\n", __func__); - rte_free(port); - return NULL; - } - port->in_ptr->dev = port; - - for (ii = 0; ii < DPVS_MAX_LCORE; ii++) { - INIT_LIST_HEAD(&port->in_ptr->ifa_list[ii]); - INIT_LIST_HEAD(&port->in_ptr->ifm_list[ii]); - } - - if (tc_init_dev(port) != EDPVS_OK) { - RTE_LOG(ERR, NETIF, "%s: fail to init TC\n", __func__); - rte_free(port); - return NULL; - } - - return port; -} - struct netif_port* netif_port_get(portid_t id) { int hash = port_tab_hashkey(id); @@ -4336,14 +4266,42 @@ static char *find_conf_kni_name(portid_t id) return NULL; } +static void dpdk_port_setup(struct netif_port *dev) +{ + dev->type = PORT_TYPE_GENERAL; + dev->netif_ops = &dpdk_netif_ops; + dev->socket = rte_eth_dev_socket_id(dev->id); + dev->dev_conf = default_port_conf; + dev->bond = (union netif_bond *)(dev + 1); + + rte_eth_macaddr_get(dev->id, &dev->addr); + rte_eth_dev_get_mtu(dev->id, &dev->mtu); + rte_eth_dev_info_get(dev->id, &dev->dev_info); + setup_dev_of_flags(dev); +} + +static void bond_port_setup(struct netif_port *dev) +{ + dev->type = PORT_TYPE_BOND_MASTER; + dev->netif_ops = &bond_netif_ops; + dev->socket = rte_eth_dev_socket_id(dev->id); + dev->dev_conf = default_port_conf; + dev->bond = (union netif_bond *)(dev + 1); + + rte_eth_macaddr_get(dev->id, &dev->addr); + rte_eth_dev_get_mtu(dev->id, &dev->mtu); + rte_eth_dev_info_get(dev->id, &dev->dev_info); + setup_dev_of_flags(dev); +} + /* Allocate and register all DPDK ports available */ static void netif_port_init(void) { int nports, nports_cfg; portid_t pid; struct netif_port *port; - struct rte_eth_conf this_eth_conf; char *kni_name; + char ifname[IFNAMSIZ]; nports = dpvs_rte_eth_dev_count(); if (nports <= 0) @@ -4358,17 +4316,23 @@ static void netif_port_init(void) port_tab_init(); port_ntab_init(); - this_eth_conf = default_port_conf; - kni_init(); for (pid = 0; pid < nports; pid++) { + if (port_name_alloc(pid, ifname, sizeof(ifname)) != EDPVS_OK) + rte_exit(EXIT_FAILURE, "Port name allocation failed, exiting...\n"); + /* queue number will be filled on device start */ - port = netif_rte_port_alloc(pid, 0, 0, &this_eth_conf); + port = NULL; + if (is_physical_port(pid)) + port = netif_alloc(pid, sizeof(union netif_bond), ifname, 0, 0, dpdk_port_setup); + else if (is_bond_port(pid)) + port = netif_alloc(pid, sizeof(union netif_bond), ifname, 0, 0, bond_port_setup); if (!port) - rte_exit(EXIT_FAILURE, "Port allocate fail, exiting...\n"); + rte_exit(EXIT_FAILURE, "Port allocation failed, exiting...\n"); + if (netif_port_register(port) < 0) - rte_exit(EXIT_FAILURE, "Port register fail, exiting...\n"); + rte_exit(EXIT_FAILURE, "Port registration failed, exiting...\n"); } if (relate_bonding_device() < 0) @@ -4490,7 +4454,7 @@ int netif_vdevs_add(void) __func__, bond_cfg->name, bond_cfg->mode, bond_cfg->numa_node); return EDPVS_CALLBACKFAIL; } - bond_cfg->port_id = ret; /* relate port_id with port_name, used by netif_rte_port_alloc */ + bond_cfg->port_id = ret; /* relate port_id with port_name, used by port_name_alloc */ RTE_LOG(INFO, NETIF, "created bondig device %s: mode=%d, primary=%s, numa_node=%d\n", bond_cfg->name, bond_cfg->mode, bond_cfg->primary, bond_cfg->numa_node); diff --git a/src/vlan.c b/src/vlan.c index 6d657e449..e594e554f 100644 --- a/src/vlan.c +++ b/src/vlan.c @@ -228,8 +228,8 @@ int vlan_add_dev(struct netif_port *real_dev, const char *ifname, } /* allocate and register netif device */ - dev = netif_alloc(sizeof(struct vlan_dev_priv), name_buf, - real_dev->nrxq, real_dev->ntxq, vlan_setup); + dev = netif_alloc(NETIF_PORT_ID_INVALID, sizeof(struct vlan_dev_priv), + name_buf, real_dev->nrxq, real_dev->ntxq, vlan_setup); if (!dev) { err = EDPVS_NOMEM; goto out; From db72d09ee64483efdf7fdc3026cddee0d80376bf Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 6 Sep 2024 14:46:19 +0800 Subject: [PATCH 57/63] inet: fix prolems in ipv6 all-nodes and all-routers address initialization - configure the addresses only when ipv6 is enabled - configure the addresses on vlan devices - configure the addresses on ip-tunnel devices (though nothing is done) Signed-off-by: ywc689 --- include/ctrl.h | 1 + include/inetaddr.h | 8 ++++- include/ip_tunnel.h | 2 ++ include/ipv6.h | 7 ++++ src/inetaddr.c | 79 +++++++++++++++++++++++++++++++++++++-------- src/ip_gre.c | 3 +- src/ip_tunnel.c | 22 +++++++++++++ src/ipip.c | 2 ++ src/ipv6/ipv6.c | 30 +++++++++-------- src/netif.c | 17 ++++++---- src/vlan.c | 12 +++++++ 11 files changed, 148 insertions(+), 35 deletions(-) diff --git a/include/ctrl.h b/include/ctrl.h index 754e38b9d..1df3d94f3 100644 --- a/include/ctrl.h +++ b/include/ctrl.h @@ -214,6 +214,7 @@ int msg_dump(const struct dpvs_msg *msg, char *buf, int len); #define MSG_TYPE_IPSET_SET 40 #define MSG_TYPE_DEST_CHECK_NOTIFY_MASTER 41 #define MSG_TYPE_DEST_CHECK_NOTIFY_SLAVES 42 +#define MSG_TYPE_IFA_IDEVINIT 43 #define MSG_TYPE_IPVS_RANGE_START 100 /* for svc per_core, refer to service.h*/ diff --git a/include/inetaddr.h b/include/inetaddr.h index 44276659f..2e1e309a6 100644 --- a/include/inetaddr.h +++ b/include/inetaddr.h @@ -26,6 +26,11 @@ #include "dpdk.h" #include "list.h" + +enum { + IDEV_F_NO_IPV6 = 0x00000001 +}; + struct inet_device { struct netif_port *dev; struct list_head ifa_list[DPVS_MAX_LCORE]; /* inet_ifaddr list */ @@ -33,6 +38,7 @@ struct inet_device { uint32_t ifa_cnt[DPVS_MAX_LCORE]; uint32_t ifm_cnt[DPVS_MAX_LCORE]; rte_atomic32_t refcnt; /* not used yet */ + uint32_t flags; /* IDEV_F_XXX */ #define this_ifa_list ifa_list[rte_lcore_id()] #define this_ifm_list ifm_list[rte_lcore_id()] #define this_ifa_cnt ifa_cnt[rte_lcore_id()] @@ -119,7 +125,7 @@ bool inet_chk_mcast_addr(int af, struct netif_port *dev, void inet_ifaddr_dad_failure(struct inet_ifaddr *ifa); -int idev_add_mcast_init(struct netif_port *dev); +int idev_addr_init(struct inet_device *idev); int inet_addr_init(void); int inet_addr_term(void); diff --git a/include/ip_tunnel.h b/include/ip_tunnel.h index 499aefb69..9a9f366e6 100644 --- a/include/ip_tunnel.h +++ b/include/ip_tunnel.h @@ -98,6 +98,8 @@ int ip_tunnel_xmit(struct rte_mbuf *mbuf, struct netif_port *dev, int ip_tunnel_pull_header(struct rte_mbuf *mbuf, int hlen, __be16 in_proto); +int ip_tunnel_dev_init(struct netif_port *dev); +int ip_tunnel_set_mc_list(struct netif_port *dev); int ip_tunnel_get_link(struct netif_port *dev, struct rte_eth_link *link); int ip_tunnel_get_stats(struct netif_port *dev, struct rte_eth_stats *stats); int ip_tunnel_get_promisc(struct netif_port *dev, bool *promisc); diff --git a/include/ipv6.h b/include/ipv6.h index edb562d00..8cf53b02c 100644 --- a/include/ipv6.h +++ b/include/ipv6.h @@ -33,6 +33,13 @@ #define IPV6 #define RTE_LOGTYPE_IPV6 RTE_LOGTYPE_USER1 +struct ipv6_config { + unsigned disable:1; + unsigned forwarding:1; +}; + +const struct ipv6_config *ip6_config_get(void); + /* * helper functions */ diff --git a/src/inetaddr.c b/src/inetaddr.c index af19d6fcc..124c75e0f 100644 --- a/src/inetaddr.c +++ b/src/inetaddr.c @@ -24,6 +24,7 @@ #include "sa_pool.h" #include "ndisc.h" #include "route.h" +#include "ipv6.h" #include "route6.h" #include "inetaddr.h" #include "conf/inetaddr.h" @@ -232,19 +233,13 @@ static int ifa_add_del_mcast(struct inet_ifaddr *ifa, bool add, bool is_master) } /* add ipv6 multicast address after port start */ -static int __idev_add_mcast_init(void *args) +static int __idev_inet6_mcast_init(struct netif_port *dev) { int err; - struct inet_device *idev; union inet_addr all_nodes, all_routers; struct rte_ether_addr eaddr_nodes, eaddr_routers; - lcoreid_t cid = rte_lcore_id(); - bool is_master = (cid == g_master_lcore_id); - struct netif_port *dev = (struct netif_port *) args; - - if (cid >= DPVS_MAX_LCORE) - return EDPVS_OK; - idev = dev_get_idev(dev); + struct inet_device *idev = dev_get_idev(dev); + bool is_master = (rte_lcore_id() == g_master_lcore_id); memset(&eaddr_nodes, 0, sizeof(eaddr_nodes)); memset(&eaddr_routers, 0, sizeof(eaddr_routers)); @@ -290,16 +285,54 @@ static int __idev_add_mcast_init(void *args) return err; } -int idev_add_mcast_init(struct netif_port *dev) +static int __idev_addr_init(void *args) +{ + struct inet_device *idev = args; + assert(idev != NULL && idev->dev != NULL); + + if (rte_lcore_id() >= DPVS_MAX_LCORE) + return EDPVS_OK; + + return __idev_inet6_mcast_init(idev->dev); +} + +int idev_addr_init(struct inet_device *idev) { int err; lcoreid_t cid; + struct dpvs_msg *msg; + + // only ipv6 needs address initialization now + if (ip6_config_get()->disable || (idev->flags & IDEV_F_NO_IPV6)) + return EDPVS_OK; + + if (rte_lcore_id() != rte_get_main_lcore()) + return EDPVS_NOTSUPP; - rte_eal_mp_remote_launch(__idev_add_mcast_init, dev, CALL_MAIN); - RTE_LCORE_FOREACH_WORKER(cid) { - err = rte_eal_wait_lcore(cid); - if (unlikely(err < 0)) + // do it on master lcore + err = __idev_addr_init(idev); + if (err != EDPVS_OK) + return err; + + // do it on slave lcores + if (dpvs_state_get() == DPVS_STATE_NORMAL) { + msg = msg_make(MSG_TYPE_IFA_IDEVINIT, ifa_msg_seq(), DPVS_MSG_MULTICAST, + rte_lcore_id(), sizeof(idev), &idev); + if (unlikely(!msg)) + return EDPVS_NOMEM; + err = multicast_msg_send(msg, DPVS_MSG_F_ASYNC, NULL); + if (err != EDPVS_OK) { + msg_destroy(&msg); return err; + } + msg_destroy(&msg); + } else { + rte_eal_mp_remote_launch(__idev_addr_init, idev, SKIP_MAIN); + RTE_LCORE_FOREACH_WORKER(cid) { + err = rte_eal_wait_lcore(cid); + if (unlikely(err < 0)) + return err; + } } return EDPVS_OK; @@ -1346,6 +1379,17 @@ static int ifa_msg_sync_cb(struct dpvs_msg *msg) return EDPVS_OK; } +static int ifa_msg_idevinit_cb(struct dpvs_msg *msg) +{ + struct inet_device *idev; + + if (unlikely(!msg || msg->len != sizeof(idev))) + return EDPVS_INVAL; + idev = *((struct inet_device **)(msg->data)); + + return __idev_addr_init(idev); +} + static int __inet_addr_add(const struct ifaddr_action *param) { int err; @@ -2115,6 +2159,13 @@ static struct dpvs_msg_type ifa_msg_types[] = { //.cid = rte_get_main_lcore(), .unicast_msg_cb = ifa_msg_sync_cb, .multicast_msg_cb = NULL + }, + { + .type = MSG_TYPE_IFA_IDEVINIT, + .prio = MSG_PRIO_NORM, + .mode = DPVS_MSG_MULTICAST, + .unicast_msg_cb = ifa_msg_idevinit_cb, + .multicast_msg_cb = NULL, } }; diff --git a/src/ip_gre.c b/src/ip_gre.c index 43fd31b8b..dbfdad7c7 100644 --- a/src/ip_gre.c +++ b/src/ip_gre.c @@ -233,12 +233,13 @@ static int gre_dev_init(struct netif_port *dev) tnl->hlen = gre_calc_hlen(tnl->params.o_flags); - return EDPVS_OK; + return ip_tunnel_dev_init(dev); } static struct netif_ops gre_dev_ops = { .op_init = gre_dev_init, .op_xmit = gre_xmit, + .op_set_mc_list = ip_tunnel_set_mc_list, .op_get_link = ip_tunnel_get_link, .op_get_stats = ip_tunnel_get_stats, .op_get_promisc = ip_tunnel_get_promisc, diff --git a/src/ip_tunnel.c b/src/ip_tunnel.c index a0e66bbbf..415b9bfb9 100644 --- a/src/ip_tunnel.c +++ b/src/ip_tunnel.c @@ -207,6 +207,8 @@ static struct netif_port *tunnel_create(struct ip_tunnel_tab *tab, dev->flag &= ~NETIF_PORT_FLAG_TX_UDP_CSUM_OFFLOAD; dev->flag &= ~NETIF_PORT_FLAG_LLDP; + dev->in_ptr->flags |= IDEV_F_NO_IPV6; + err = netif_port_register(dev); if (err != EDPVS_OK) { netif_free(dev); @@ -900,6 +902,26 @@ int ip_tunnel_pull_header(struct rte_mbuf *mbuf, int hlen, __be16 in_proto) return EDPVS_OK; } +int ip_tunnel_dev_init(struct netif_port *dev) +{ + int err; + struct ip_tunnel *tnl = netif_priv(dev); + + err = idev_addr_init(tnl->dev->in_ptr); + if (err != EDPVS_OK) + return err; + + return EDPVS_OK; +} + +int ip_tunnel_set_mc_list(struct netif_port *dev) +{ + // IP tunnel devices need no hw multicast address, + // and should always return success + + return EDPVS_OK; +} + int ip_tunnel_get_link(struct netif_port *dev, struct rte_eth_link *link) { struct ip_tunnel *tnl = netif_priv(dev); diff --git a/src/ipip.c b/src/ipip.c index 8b25f0c8e..8a7ffe100 100644 --- a/src/ipip.c +++ b/src/ipip.c @@ -46,7 +46,9 @@ static int ipip_xmit(struct rte_mbuf *mbuf, struct netif_port *dev) } static struct netif_ops ipip_dev_ops = { + .op_init = ip_tunnel_dev_init, .op_xmit = ipip_xmit, + .op_set_mc_list = ip_tunnel_set_mc_list, .op_get_link = ip_tunnel_get_link, .op_get_stats = ip_tunnel_get_stats, .op_get_promisc = ip_tunnel_get_promisc, diff --git a/src/ipv6/ipv6.c b/src/ipv6/ipv6.c index 1fa712110..3fa58485a 100644 --- a/src/ipv6/ipv6.c +++ b/src/ipv6/ipv6.c @@ -46,8 +46,12 @@ static rte_rwlock_t inet6_prot_lock; /* * IPv6 configures with default values. */ -static bool conf_ipv6_forwarding = false; -static bool conf_ipv6_disable = false; +static struct ipv6_config ip6_configs; + +const struct ipv6_config *ip6_config_get(void) +{ + return &ip6_configs; +}; /* * IPv6 statistics @@ -115,13 +119,13 @@ static void ip6_conf_forward(vector_t tokens) assert(str); if (strcasecmp(str, "on") == 0) - conf_ipv6_forwarding = true; + ip6_configs.forwarding = 1; else if (strcasecmp(str, "off") == 0) - conf_ipv6_forwarding = false; + ip6_configs.forwarding = 0; else RTE_LOG(WARNING, IPV6, "invalid ipv6:forwarding %s\n", str); - RTE_LOG(INFO, IPV6, "ipv6:forwarding = %s\n", conf_ipv6_forwarding ? "on" : "off"); + RTE_LOG(INFO, IPV6, "ipv6:forwarding = %s\n", ip6_configs.forwarding ? "on" : "off"); FREE_PTR(str); } @@ -133,13 +137,13 @@ static void ip6_conf_disable(vector_t tokens) assert(str); if (strcasecmp(str, "on") == 0) - conf_ipv6_disable = true; + ip6_configs.disable = 1; else if (strcasecmp(str, "off") == 0) - conf_ipv6_disable = false; + ip6_configs.disable = 0; else RTE_LOG(WARNING, IPV6, "invalid ipv6:disable %s\n", str); - RTE_LOG(INFO, IPV6, "ipv6:disable = %s\n", conf_ipv6_disable ? "on" : "off"); + RTE_LOG(INFO, IPV6, "ipv6: %s\n", ip6_configs.disable ? "disabled" : "enabled"); FREE_PTR(str); } @@ -371,7 +375,7 @@ int ip6_output(struct rte_mbuf *mbuf) mbuf->port = dev->id; iftraf_pkt_out(AF_INET6, mbuf, dev); - if (unlikely(conf_ipv6_disable)) { + if (unlikely(ip6_configs.disable)) { IP6_INC_STATS(outdiscards); if (rt) route6_put(rt); @@ -411,7 +415,7 @@ static int ip6_forward(struct rte_mbuf *mbuf) int addrtype; uint32_t mtu; - if (!conf_ipv6_forwarding) + if (!ip6_configs.forwarding) goto error; if (mbuf->packet_type != ETH_PKT_HOST) @@ -539,7 +543,7 @@ static int ip6_rcv(struct rte_mbuf *mbuf, struct netif_port *dev) IP6_UPD_PO_STATS(in, mbuf->pkt_len); iftraf_pkt_in(AF_INET6, mbuf, dev); - if (unlikely(conf_ipv6_disable)) { + if (unlikely(ip6_configs.disable)) { IP6_INC_STATS(indiscards); goto drop; } @@ -815,8 +819,8 @@ void ipv6_keyword_value_init(void) /* KW_TYPE_INIT keyword */ } /* KW_TYPE NORMAL keyword */ - conf_ipv6_forwarding = false; - conf_ipv6_disable = false; + ip6_configs.forwarding = 0; + ip6_configs.disable = 0; route6_keyword_value_init(); } diff --git a/src/netif.c b/src/netif.c index f0113c519..680436caa 100644 --- a/src/netif.c +++ b/src/netif.c @@ -4013,9 +4013,9 @@ int netif_port_start(struct netif_port *port) if (port->netif_ops->op_update_addr) port->netif_ops->op_update_addr(port); - /* add in6_addr multicast address */ - if ((ret = idev_add_mcast_init(port)) != EDPVS_OK) { - RTE_LOG(WARNING, NETIF, "%s: idev_add_mcast_init failed -- %d(%s)\n", + /* ipv6 default addresses initialization */ + if ((ret = idev_addr_init(port->in_ptr)) != EDPVS_OK) { + RTE_LOG(WARNING, NETIF, "%s: idev_addr_init failed -- %d(%s)\n", __func__, ret, dpvs_strerror(ret)); return ret; } @@ -4053,7 +4053,7 @@ int netif_port_register(struct netif_port *port) { struct netif_port *cur; int hash, nhash; - int err = EDPVS_OK; + int err; if (unlikely(NULL == port)) return EDPVS_INVAL; @@ -4076,10 +4076,15 @@ int netif_port_register(struct netif_port *port) list_add_tail(&port->nlist, &port_ntab[nhash]); g_nports++; - if (port->netif_ops->op_init) + if (port->netif_ops->op_init) { err = port->netif_ops->op_init(port); + if (err != EDPVS_OK) { + netif_port_unregister(port); + return err; + } + } - return err; + return EDPVS_OK; } int netif_port_unregister(struct netif_port *port) diff --git a/src/vlan.c b/src/vlan.c index e594e554f..0d350d07c 100644 --- a/src/vlan.c +++ b/src/vlan.c @@ -75,6 +75,17 @@ static int alloc_vlan_info(struct netif_port *dev) return EDPVS_OK; } +static int vlan_dev_init(struct netif_port *dev) +{ + int err; + + err = idev_addr_init(dev->in_ptr); + if (err != EDPVS_OK) + return err; + + return EDPVS_OK; +} + static int vlan_xmit(struct rte_mbuf *mbuf, struct netif_port *dev) { struct vlan_dev_priv *vlan = netif_priv(dev); @@ -171,6 +182,7 @@ static int vlan_get_stats(struct netif_port *dev, struct rte_eth_stats *stats) } static struct netif_ops vlan_netif_ops = { + .op_init = vlan_dev_init, .op_xmit = vlan_xmit, .op_set_mc_list = vlan_set_mc_list, .op_get_queue = vlan_get_queue, From ed1a6e955d047056355956230cbf9d9d81ca2496 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Mon, 9 Sep 2024 18:21:36 +0800 Subject: [PATCH 58/63] inetaddr: ipv6 link-local address auto configuration Referencing linux kernel(https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt), dpvs supports 4 addr_gen_mode - eui64 - none - stable-privacy - random Signed-off-by: ywc689 --- conf/dpvs.conf.items | 3 + include/conf/inetaddr.h | 1 + include/inetaddr.h | 8 ++- include/ipv6.h | 16 +++++ include/linux_ipv6.h | 13 ++++ src/inetaddr.c | 152 ++++++++++++++++++++++++++++++++++------ src/ip_tunnel.c | 8 ++- src/ipv6/ipv6.c | 83 ++++++++++++++++++++-- src/netif.c | 1 + src/vlan.c | 8 ++- 10 files changed, 263 insertions(+), 30 deletions(-) diff --git a/conf/dpvs.conf.items b/conf/dpvs.conf.items index 448b40044..80f2e123a 100644 --- a/conf/dpvs.conf.items +++ b/conf/dpvs.conf.items @@ -194,6 +194,9 @@ ipv4_defs { ipv6_defs { disable off forwarding off + addr_gen_mode eui64 + stable_secret "" <128-bit hexadecimal string, used in stable-privacy mode > + route6 { method "hlist" <"hlist"/"lpm"> recycle_time 10 <10, 1-36000> diff --git a/include/conf/inetaddr.h b/include/conf/inetaddr.h index 7a82b0d94..252e6f3ee 100644 --- a/include/conf/inetaddr.h +++ b/include/conf/inetaddr.h @@ -34,6 +34,7 @@ enum { /* leverage IFA_F_XXX in linux/if_addr.h*/ #define IFA_F_SAPOOL 0x10000 /* if address with sockaddr pool */ +#define IFA_F_LINKLOCAL 0x20000 /* ipv6 link-local address */ /* ifa command flags */ #define IFA_F_OPS_VERBOSE 0x0001 diff --git a/include/inetaddr.h b/include/inetaddr.h index 2e1e309a6..a96b62518 100644 --- a/include/inetaddr.h +++ b/include/inetaddr.h @@ -28,7 +28,8 @@ enum { - IDEV_F_NO_IPV6 = 0x00000001 + IDEV_F_NO_IPV6 = 0x00000001, + IDEV_F_NO_ROUTE = 0x00000002, }; struct inet_device { @@ -125,8 +126,13 @@ bool inet_chk_mcast_addr(int af, struct netif_port *dev, void inet_ifaddr_dad_failure(struct inet_ifaddr *ifa); +struct inet_device *dev_get_idev(const struct netif_port *dev); + +void idev_put(struct inet_device *idev); + int idev_addr_init(struct inet_device *idev); + int inet_addr_init(void); int inet_addr_term(void); diff --git a/include/ipv6.h b/include/ipv6.h index 8cf53b02c..7a95736f0 100644 --- a/include/ipv6.h +++ b/include/ipv6.h @@ -33,9 +33,25 @@ #define IPV6 #define RTE_LOGTYPE_IPV6 RTE_LOGTYPE_USER1 +enum ip6_addr_gen_mode { + IP6_ADDR_GEN_MODE_EUI64 = 1, + IP6_ADDR_GEN_MODE_NONE, + IP6_ADDR_GEN_MODE_STABLE_PRIVACY, + IP6_ADDR_GEN_MODE_RANDOM, + IP6_ADDR_GFN_MODE_MAX = 64, +}; + +struct ipv6_stable_secret { + bool initialized; + struct in6_addr secret; +}; + struct ipv6_config { unsigned disable:1; unsigned forwarding:1; + unsigned addr_gen_mode:6; + struct ipv6_stable_secret secret_stable; + struct ipv6_stable_secret secret_random; }; const struct ipv6_config *ip6_config_get(void); diff --git a/include/linux_ipv6.h b/include/linux_ipv6.h index 05bc4ae3b..6d19b1b36 100644 --- a/include/linux_ipv6.h +++ b/include/linux_ipv6.h @@ -492,6 +492,19 @@ static inline int ipv6_saddr_preferred(int type) return 0; } +static inline bool ipv6_reserved_interfaceid(const struct in6_addr *addr) +{ + if ((addr->s6_addr32[2] | addr->s6_addr32[3]) == 0) + return true; + if (addr->s6_addr32[2] == htonl(0x02005eff) && + ((addr->s6_addr32[3] & htonl(0xfe000000)) == htonl(0xfe000000))) + return true; + if (addr->s6_addr32[2] == htonl(0xfdffffff) && + ((addr->s6_addr32[3] & htonl(0xffffff80)) == htonl(0xffffff80))) + return true; + return false; +} + #ifdef __DPVS__ /*functions below were edited from addrconf.c*/ diff --git a/src/inetaddr.c b/src/inetaddr.c index 124c75e0f..fb833436a 100644 --- a/src/inetaddr.c +++ b/src/inetaddr.c @@ -16,6 +16,7 @@ * */ #include +#include #include "dpdk.h" #include "ctrl.h" #include "netif.h" @@ -76,14 +77,14 @@ static uint32_t ifa_msg_seq(void) return counter++; } -static inline struct inet_device *dev_get_idev(const struct netif_port *dev) +struct inet_device *dev_get_idev(const struct netif_port *dev) { assert(dev && dev->in_ptr); rte_atomic32_inc(&dev->in_ptr->refcnt); return dev->in_ptr; } -static inline void idev_put(struct inet_device *idev) +void idev_put(struct inet_device *idev) { rte_atomic32_dec(&idev->refcnt); } @@ -233,12 +234,11 @@ static int ifa_add_del_mcast(struct inet_ifaddr *ifa, bool add, bool is_master) } /* add ipv6 multicast address after port start */ -static int __idev_inet6_mcast_init(struct netif_port *dev) +static int __idev_inet6_mcast_init(struct inet_device *idev) { int err; union inet_addr all_nodes, all_routers; struct rte_ether_addr eaddr_nodes, eaddr_routers; - struct inet_device *idev = dev_get_idev(dev); bool is_master = (rte_lcore_id() == g_master_lcore_id); memset(&eaddr_nodes, 0, sizeof(eaddr_nodes)); @@ -270,7 +270,6 @@ static int __idev_inet6_mcast_init(struct netif_port *dev) goto free_idev_routers; } - idev_put(idev); return EDPVS_OK; free_idev_routers: @@ -281,43 +280,156 @@ static int __idev_inet6_mcast_init(struct netif_port *dev) free_idev_nodes: idev_mc_del(AF_INET6, idev, &all_nodes); errout: - idev_put(idev); return err; } +static int inet6_addr_gen_eui64(struct inet_device *idev, struct in6_addr *addr) +{ + unsigned char hwaddr[6]; + unsigned char *eui = &addr->s6_addr[8]; + + rte_memcpy(hwaddr, &idev->dev->addr, 6); + eui[0] = hwaddr[0] ^ 0x02; + eui[1] = hwaddr[1]; + eui[2] = hwaddr[2]; + eui[3] = 0xFF; + eui[4] = 0xFE; + eui[5] = hwaddr[3]; + eui[6] = hwaddr[4]; + eui[7] = hwaddr[5]; + + return EDPVS_OK; +} + +static int inet6_addr_gen_stable(struct in6_addr secret, struct inet_device *idev, struct in6_addr *addr) +{ +#define MAX_RETRY 8 + struct in6_addr temp; + union { + unsigned char data[SHA256_DIGEST_LENGTH]; + uint32_t data_word[2]; + } md; + struct { + struct in6_addr secret; + uint32_t prefix[2]; + struct rte_ether_addr hwaddr; + uint8_t dad_count; + } __rte_packed data; + uint8_t dad_count = 0; + + memset(&data, 0, sizeof(data)); + data.secret = secret; + data.prefix[0] = addr->s6_addr32[0]; + data.prefix[1] = addr->s6_addr32[1]; + data.hwaddr = idev->dev->addr; + while (1) { + data.dad_count = dad_count++; + memset(&md, 0, sizeof(md)); + SHA512((const unsigned char*)&data, sizeof(data), md.data); + temp = *addr; + temp.s6_addr32[2] = md.data_word[0]; + temp.s6_addr32[3] = md.data_word[1]; + if (!ipv6_reserved_interfaceid(&temp)) + break; + if (dad_count >= MAX_RETRY) + return EDPVS_RESOURCE; + } + + *addr = temp; + return EDPVS_OK; +} + +static int inet6_link_local_addr_gen(struct inet_device *idev, struct in6_addr *addr) +{ + const struct ipv6_config *ip6cfg = ip6_config_get(); + + ipv6_addr_set(addr, htonl(0xFE800000), 0, 0, 0); + switch (ip6cfg->addr_gen_mode) { + case IP6_ADDR_GEN_MODE_EUI64: + return inet6_addr_gen_eui64(idev, addr); + case IP6_ADDR_GEN_MODE_NONE: + return EDPVS_DISABLED; + case IP6_ADDR_GEN_MODE_STABLE_PRIVACY: + if (ip6cfg->secret_stable.initialized) + return inet6_addr_gen_stable(ip6cfg->secret_stable.secret, idev, addr); + // fallthrough + case IP6_ADDR_GEN_MODE_RANDOM: + return inet6_addr_gen_stable(ip6cfg->secret_random.secret, idev, addr); + default: + return EDPVS_NOTSUPP; + } + + return EDPVS_OK; +} + +static int ifa_entry_add(const struct ifaddr_action *param); +static int __inet6_link_local_addr_config(struct inet_device *idev, const struct in6_addr *addr) +{ + struct ifaddr_action param; + + memset(¶m, 0, sizeof(param)); + fill_ifaddr_action(AF_INET6, idev->dev, (union inet_addr *)addr, 64, NULL, + 0, 0, IFA_SCOPE_LINK, IFA_F_LINKLOCAL, INET_ADDR_ADD, ¶m); + return ifa_entry_add(¶m); +} + +struct idev_addr_init_args { + struct inet_device *idev; + struct in6_addr link_local_addr; +}; + static int __idev_addr_init(void *args) { - struct inet_device *idev = args; - assert(idev != NULL && idev->dev != NULL); + int err; + struct idev_addr_init_args *param = args; + + assert(param && param->idev && param->idev->dev); if (rte_lcore_id() >= DPVS_MAX_LCORE) return EDPVS_OK; - return __idev_inet6_mcast_init(idev->dev); + err = __inet6_link_local_addr_config(param->idev, ¶m->link_local_addr); + if (err != EDPVS_OK) + return err; + + return __idev_inet6_mcast_init(param->idev); } int idev_addr_init(struct inet_device *idev) { int err; - lcoreid_t cid; + lcoreid_t cid, tcid; struct dpvs_msg *msg; + struct idev_addr_init_args args; // only ipv6 needs address initialization now if (ip6_config_get()->disable || (idev->flags & IDEV_F_NO_IPV6)) return EDPVS_OK; - if (rte_lcore_id() != rte_get_main_lcore()) + if (idev->flags & IDEV_F_NO_ROUTE) + return EDPVS_OK; + + cid = rte_lcore_id(); + if (cid != rte_get_main_lcore()) return EDPVS_NOTSUPP; + args.idev = idev; + err = inet6_link_local_addr_gen(idev, &args.link_local_addr); + if (err != EDPVS_OK) { + if (EDPVS_DISABLED == err) + return EDPVS_OK; + return err; + } + // do it on master lcore - err = __idev_addr_init(idev); + err = __idev_addr_init(&args); if (err != EDPVS_OK) return err; // do it on slave lcores if (dpvs_state_get() == DPVS_STATE_NORMAL) { msg = msg_make(MSG_TYPE_IFA_IDEVINIT, ifa_msg_seq(), DPVS_MSG_MULTICAST, - rte_lcore_id(), sizeof(idev), &idev); + cid, sizeof(args), &args); if (unlikely(!msg)) return EDPVS_NOMEM; err = multicast_msg_send(msg, DPVS_MSG_F_ASYNC, NULL); @@ -327,9 +439,9 @@ int idev_addr_init(struct inet_device *idev) } msg_destroy(&msg); } else { - rte_eal_mp_remote_launch(__idev_addr_init, idev, SKIP_MAIN); - RTE_LCORE_FOREACH_WORKER(cid) { - err = rte_eal_wait_lcore(cid); + rte_eal_mp_remote_launch(__idev_addr_init, &args, SKIP_MAIN); + RTE_LCORE_FOREACH_WORKER(tcid) { + err = rte_eal_wait_lcore(tcid); if (unlikely(err < 0)) return err; } @@ -1381,13 +1493,13 @@ static int ifa_msg_sync_cb(struct dpvs_msg *msg) static int ifa_msg_idevinit_cb(struct dpvs_msg *msg) { - struct inet_device *idev; + struct idev_addr_init_args *param; - if (unlikely(!msg || msg->len != sizeof(idev))) + if (unlikely(!msg || msg->len != sizeof(*param))) return EDPVS_INVAL; - idev = *((struct inet_device **)(msg->data)); + param = (struct idev_addr_init_args *)(msg->data); - return __idev_addr_init(idev); + return __idev_addr_init(param); } static int __inet_addr_add(const struct ifaddr_action *param) diff --git a/src/ip_tunnel.c b/src/ip_tunnel.c index 415b9bfb9..e26f3eee7 100644 --- a/src/ip_tunnel.c +++ b/src/ip_tunnel.c @@ -906,11 +906,15 @@ int ip_tunnel_dev_init(struct netif_port *dev) { int err; struct ip_tunnel *tnl = netif_priv(dev); + struct inet_device *idev = dev_get_idev(tnl->dev); - err = idev_addr_init(tnl->dev->in_ptr); - if (err != EDPVS_OK) + err = idev_addr_init(idev); + if (err != EDPVS_OK) { + idev_put(idev); return err; + } + idev_put(idev); return EDPVS_OK; } diff --git a/src/ipv6/ipv6.c b/src/ipv6/ipv6.c index 3fa58485a..37da08fb6 100644 --- a/src/ipv6/ipv6.c +++ b/src/ipv6/ipv6.c @@ -112,7 +112,7 @@ static void ip6_prot_init(void) rte_rwlock_write_unlock(&inet6_prot_lock); } -static void ip6_conf_forward(vector_t tokens) +static void ip6_forwarding_handler(vector_t tokens) { char *str = set_value(tokens); @@ -130,7 +130,7 @@ static void ip6_conf_forward(vector_t tokens) FREE_PTR(str); } -static void ip6_conf_disable(vector_t tokens) +static void ip6_disable_handler(vector_t tokens) { char *str = set_value(tokens); @@ -143,11 +143,78 @@ static void ip6_conf_disable(vector_t tokens) else RTE_LOG(WARNING, IPV6, "invalid ipv6:disable %s\n", str); - RTE_LOG(INFO, IPV6, "ipv6: %s\n", ip6_configs.disable ? "disabled" : "enabled"); + RTE_LOG(INFO, IPV6, "ipv6:disable=%s\n", ip6_configs.disable ? "disabled" : "enabled"); FREE_PTR(str); } +static void ip6_addr_gen_mode_handler(vector_t tokens) +{ + char *str = set_value(tokens); + + assert(str); + + if (!strcasecmp(str, "eui64")) + ip6_configs.addr_gen_mode = IP6_ADDR_GEN_MODE_EUI64; + else if (!strcasecmp(str, "none")) + ip6_configs.addr_gen_mode = IP6_ADDR_GEN_MODE_NONE; + else if (!strcasecmp(str, "stable-privacy")) + ip6_configs.addr_gen_mode = IP6_ADDR_GEN_MODE_STABLE_PRIVACY; + else if (!strcasecmp(str, "random")) + ip6_configs.addr_gen_mode = IP6_ADDR_GEN_MODE_RANDOM; + else + RTE_LOG(WARNING, IPV6, "invalid ipv6:addr_gen_mode:%s\n", str); + + RTE_LOG(INFO, IPV6, "ipv6:addr_gen_mode=%s\n", str); + + FREE_PTR(str); +} + +static void ip6_stable_secret_handler(vector_t tokens) +{ + bool valid = true; + size_t i, len; + char *str = set_value(tokens); + + assert(str); + len = strlen(str); + if (len < 32) { + valid = false; + } else { + for (i = 0; i < 32; i++) { + if (!isxdigit(str[i])) { + valid = false; + break; + } + } + } + if (!valid) { + RTE_LOG(WARNING, IPV6, "invalid ipv6:stable_secret %s, " + "a 128-bit hexadecimal string required\n", str); + FREE_PTR(str); + return; + } + + if (hexstr2binary(str, 32, (uint8_t *)(&ip6_configs.secret_stable.secret), 16) == 16) + ip6_configs.secret_stable.initialized = true; + else + RTE_LOG(WARNING, IPV6, "fail to tranlate ipv6:stable_secret %s into binary\n", str); + RTE_LOG(INFO, IPV6, "ipv6:stable_secret configured"); + + FREE_PTR(str); +} + +static inline void ip6_gen_mode_random_init(void) +{ + const char hex_chars[] = "0123456789abcdef"; + char *buf = (char *)(&ip6_configs.secret_random.secret); + int i; + + for (i = 0; i < 16; i++) + buf[i] = hex_chars[random() % 16]; + ip6_configs.secret_random.initialized = true; +} + /* refer linux:ip6_input_finish() */ static int ip6_local_in_fin(struct rte_mbuf *mbuf) { @@ -660,6 +727,8 @@ int ipv6_init(void) /* htons, cpu_to_be16 not work when struct initialization :( */ ip6_pkt_type.type = htons(RTE_ETHER_TYPE_IPV6); + ip6_gen_mode_random_init(); + err = netif_register_pkt(&ip6_pkt_type); if (err) goto reg_pkt_err; @@ -821,6 +890,8 @@ void ipv6_keyword_value_init(void) /* KW_TYPE NORMAL keyword */ ip6_configs.forwarding = 0; ip6_configs.disable = 0; + ip6_configs.addr_gen_mode = IP6_ADDR_GEN_MODE_EUI64; + ip6_configs.secret_stable.initialized = false; route6_keyword_value_init(); } @@ -828,8 +899,10 @@ void ipv6_keyword_value_init(void) void install_ipv6_keywords(void) { install_keyword_root("ipv6_defs", NULL); - install_keyword("forwarding", ip6_conf_forward, KW_TYPE_NORMAL); - install_keyword("disable", ip6_conf_disable, KW_TYPE_NORMAL); + install_keyword("forwarding", ip6_forwarding_handler, KW_TYPE_NORMAL); + install_keyword("disable", ip6_disable_handler, KW_TYPE_NORMAL); + install_keyword("addr_gen_mode", ip6_addr_gen_mode_handler, KW_TYPE_NORMAL); + install_keyword("stable_secret", ip6_stable_secret_handler, KW_TYPE_NORMAL); install_route6_keywords(); } diff --git a/src/netif.c b/src/netif.c index 680436caa..5d8fe0b3d 100644 --- a/src/netif.c +++ b/src/netif.c @@ -4164,6 +4164,7 @@ static int relate_bonding_device(void) } sport->type = PORT_TYPE_BOND_SLAVE; sport->bond->slave.master = mport; + sport->in_ptr->flags |= IDEV_F_NO_ROUTE; } mport->bond->master.slave_nb = i; } diff --git a/src/vlan.c b/src/vlan.c index 0d350d07c..955b0b840 100644 --- a/src/vlan.c +++ b/src/vlan.c @@ -78,11 +78,15 @@ static int alloc_vlan_info(struct netif_port *dev) static int vlan_dev_init(struct netif_port *dev) { int err; + struct inet_device *idev = dev_get_idev(dev); - err = idev_addr_init(dev->in_ptr); - if (err != EDPVS_OK) + err = idev_addr_init(idev); + if (err != EDPVS_OK) { + idev_put(idev); return err; + } + idev_put(idev); return EDPVS_OK; } From 5b4e47cbdaa02742480bc8fa4182800366c22417 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 2 Aug 2024 13:44:31 +0800 Subject: [PATCH 59/63] patch: add patches of dpdk-stable-20.11.10 and alter default dpdk version of dpdk build script Signed-off-by: ywc689 --- ...link-event-for-multicast-driver-part.patch | 128 ++++ ...dump-change-dpdk-pdump-tool-for-dpvs.patch | 555 ++++++++++++++++++ ...3-debug-enable-dpdk-eal-memory-debug.patch | 59 ++ ...w-patch-ixgbe-fdir-rte_flow-for-dpvs.patch | 256 ++++++++ ...low-slaves-from-different-numa-nodes.patch | 50 ++ ...lem-in-mode-4-dropping-multicast-pac.patch | 79 +++ ...ends-packets-with-user-specified-sal.patch | 92 +++ scripts/dpdk-build.sh | 19 +- 8 files changed, 1222 insertions(+), 16 deletions(-) create mode 100644 patch/dpdk-stable-20.11.10/0001-kni-use-netlink-event-for-multicast-driver-part.patch create mode 100644 patch/dpdk-stable-20.11.10/0002-pdump-change-dpdk-pdump-tool-for-dpvs.patch create mode 100644 patch/dpdk-stable-20.11.10/0003-debug-enable-dpdk-eal-memory-debug.patch create mode 100644 patch/dpdk-stable-20.11.10/0004-ixgbe_flow-patch-ixgbe-fdir-rte_flow-for-dpvs.patch create mode 100644 patch/dpdk-stable-20.11.10/0005-bonding-allow-slaves-from-different-numa-nodes.patch create mode 100644 patch/dpdk-stable-20.11.10/0006-bonding-fix-problem-in-mode-4-dropping-multicast-pac.patch create mode 100644 patch/dpdk-stable-20.11.10/0007-bonding-device-sends-packets-with-user-specified-sal.patch diff --git a/patch/dpdk-stable-20.11.10/0001-kni-use-netlink-event-for-multicast-driver-part.patch b/patch/dpdk-stable-20.11.10/0001-kni-use-netlink-event-for-multicast-driver-part.patch new file mode 100644 index 000000000..8f9c28635 --- /dev/null +++ b/patch/dpdk-stable-20.11.10/0001-kni-use-netlink-event-for-multicast-driver-part.patch @@ -0,0 +1,128 @@ +From 3e182c106d61863a55e35425e2afefcc222f8f92 Mon Sep 17 00:00:00 2001 +From: yuwenchao +Date: Thu, 1 Aug 2024 17:18:30 +0800 +Subject: [PATCH 1/7] kni: use netlink event for multicast (driver part) + +Signed-off-by: yuwenchao +--- + kernel/linux/kni/kni_net.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 76 insertions(+) + +diff --git a/kernel/linux/kni/kni_net.c b/kernel/linux/kni/kni_net.c +index 779ee34..31e9e39 100644 +--- a/kernel/linux/kni/kni_net.c ++++ b/kernel/linux/kni/kni_net.c +@@ -17,6 +17,8 @@ + #include + #include + #include ++#include ++#include + #include + + #include +@@ -147,6 +149,7 @@ + ret_val = wait_event_interruptible_timeout(kni->wq, + kni_fifo_count(kni->resp_q), 3 * HZ); + if (signal_pending(current) || ret_val <= 0) { ++ pr_err("%s: wait_event_interruptible timeout\n", __func__); + ret = -ETIME; + goto fail; + } +@@ -690,6 +693,77 @@ void kni_net_release_fifo_phy(struct kni_dev *kni) + return (ret == 0) ? req.result : ret; + } + ++static size_t ++kni_nlmsg_size(void) ++{ ++ return NLMSG_ALIGN(sizeof(struct ifaddrmsg)) ++ + nla_total_size(4) /* IFA_ADDRESS */ ++ + nla_total_size(4) /* IFA_LOCAL */ ++ + nla_total_size(4) /* IFA_BROADCAST */ ++ + nla_total_size(IFNAMSIZ) /* IFA_LABEL */ ++ + nla_total_size(4) /* IFA_FLAGS */ ++ + nla_total_size(sizeof(struct ifa_cacheinfo)); /* IFA_CACHEINFO */ ++} ++ ++static void ++kni_net_set_rx_mode(struct net_device *dev) ++{ ++ /* ++ * send event to notify user (DPDK KNI app) that multicast list changed, ++ * so that it can monitor multicast join/leave and set HW mc-addrs to ++ * kni dev accordinglly. ++ * ++ * this event is just an notification, we do not save any mc-addr here ++ * (so attribute space for us). user kni app should get maddrs after ++ * receive this notification. ++ * ++ * I was expecting kernel send some rtnl event for multicast join/leave, ++ * but it doesn't. By checking the call-chain of SIOCADDMULTI (ip maddr, ++ * manages only hardware multicast) and IP_ADD_MEMBERSHIP (ip_mc_join_group, ++ * used to for IPv4 multicast), no rtnl event sent. ++ * ++ * so as workaround, modify kni driver here to send RTM_NEWADDR. ++ * it may not suitalbe to use this event for mcast, but that should works. ++ * hope that won't affect other listener to this event. ++ * ++ * previous solution was using rte_kni_request to pass hw-maddr list to user. ++ * it "works" for times but finally memory corruption found, which is ++ * not easy to address (lock was added and reviewed). That's why we use ++ * netlink event instead. ++ */ ++ struct sk_buff *skb; ++ struct net *net = dev_net(dev); ++ struct nlmsghdr *nlh; ++ struct ifaddrmsg *ifm; ++ ++ skb = nlmsg_new(kni_nlmsg_size(), GFP_ATOMIC); ++ if (!skb) ++ return; ++ ++ /* no other event for us ? */ ++ nlh = nlmsg_put(skb, 0, 0, RTM_NEWADDR, sizeof(*ifm), 0); ++ if (!nlh) { ++ kfree_skb(skb); ++ return; ++ } ++ ++ /* just send an notification so no other info */ ++ ifm = nlmsg_data(nlh); ++ memset(ifm, 0, sizeof(*ifm)); ++ ifm->ifa_family = AF_UNSPEC; ++ ifm->ifa_prefixlen = 0; ++ ifm->ifa_flags = 0; ++ ifm->ifa_scope = RT_SCOPE_NOWHERE; ++ ifm->ifa_index = 0; ++ ++ nlmsg_end(skb, nlh); ++ ++ /* other group ? */ ++ pr_debug("%s: rx-mode/multicast-list changed\n", __func__); ++ rtnl_notify(skb, net, 0, RTNLGRP_NOTIFY, NULL, GFP_ATOMIC); ++ return; ++} ++ + static void + kni_net_change_rx_flags(struct net_device *netdev, int flags) + { +@@ -791,6 +865,7 @@ void kni_net_release_fifo_phy(struct kni_dev *kni) + + ret = kni_net_process_request(netdev, &req); + ++ pr_info("%s request returns %d!\n", __func__, ret); + return (ret == 0 ? req.result : ret); + } + +@@ -822,6 +897,7 @@ void kni_net_release_fifo_phy(struct kni_dev *kni) + .ndo_change_rx_flags = kni_net_change_rx_flags, + .ndo_start_xmit = kni_net_tx, + .ndo_change_mtu = kni_net_change_mtu, ++ .ndo_set_rx_mode = kni_net_set_rx_mode, + .ndo_tx_timeout = kni_net_tx_timeout, + .ndo_set_mac_address = kni_net_set_mac, + #ifdef HAVE_CHANGE_CARRIER_CB +-- +1.8.3.1 + diff --git a/patch/dpdk-stable-20.11.10/0002-pdump-change-dpdk-pdump-tool-for-dpvs.patch b/patch/dpdk-stable-20.11.10/0002-pdump-change-dpdk-pdump-tool-for-dpvs.patch new file mode 100644 index 000000000..5d71abd55 --- /dev/null +++ b/patch/dpdk-stable-20.11.10/0002-pdump-change-dpdk-pdump-tool-for-dpvs.patch @@ -0,0 +1,555 @@ +From 288a252c8b65ea6c811100b3472367891f298f7d Mon Sep 17 00:00:00 2001 +From: huangyichen +Date: Thu, 1 Jul 2021 21:23:50 +0800 +Subject: [PATCH 2/7] pdump: change dpdk-pdump tool for dpvs + +--- + app/pdump/main.c | 167 ++++++++++++++++++++++++++++++++++++++++--- + lib/librte_pdump/rte_pdump.c | 145 +++++++++++++++++++++++++++++++++++-- + lib/librte_pdump/rte_pdump.h | 27 +++++++ + 3 files changed, 327 insertions(+), 12 deletions(-) + +diff --git a/app/pdump/main.c b/app/pdump/main.c +index 36b14fa..5b4217e 100644 +--- a/app/pdump/main.c ++++ b/app/pdump/main.c +@@ -27,6 +27,7 @@ + #include + #include + #include ++#include + + #define CMD_LINE_OPT_PDUMP "pdump" + #define CMD_LINE_OPT_PDUMP_NUM 256 +@@ -42,6 +43,14 @@ + #define PDUMP_MSIZE_ARG "mbuf-size" + #define PDUMP_NUM_MBUFS_ARG "total-num-mbufs" + ++#define PDUMP_HOST_ARG "host" ++#define PDUMP_SRC_ARG "src-host" ++#define PDUMP_DST_ARG "dst-host" ++#define PDUMP_PROTO_PORT_AGE "proto-port" ++#define PDUMP_SPORT_ARG "src-port" ++#define PDUMP_DPORT_ARG "dst-port" ++#define PDUMP_PROTO_ARG "proto" ++ + #define VDEV_NAME_FMT "net_pcap_%s_%d" + #define VDEV_PCAP_ARGS_FMT "tx_pcap=%s" + #define VDEV_IFACE_ARGS_FMT "tx_iface=%s" +@@ -97,6 +106,13 @@ enum pdump_by { + PDUMP_RING_SIZE_ARG, + PDUMP_MSIZE_ARG, + PDUMP_NUM_MBUFS_ARG, ++ PDUMP_HOST_ARG, ++ PDUMP_SRC_ARG, ++ PDUMP_DST_ARG, ++ PDUMP_PROTO_PORT_AGE, ++ PDUMP_SPORT_ARG, ++ PDUMP_DPORT_ARG, ++ PDUMP_PROTO_ARG, + NULL + }; + +@@ -130,6 +146,7 @@ struct pdump_tuples { + enum pcap_stream rx_vdev_stream_type; + enum pcap_stream tx_vdev_stream_type; + bool single_pdump_dev; ++ struct pdump_filter *filter; + + /* stats */ + struct pdump_stats stats; +@@ -158,6 +175,11 @@ struct parse_val { + "(queue=)," + "(rx-dev= |" + " tx-dev=," ++ "[host= | src-host= |" ++ "dst-host=]," ++ "[proto=support:tcp/udp/icmp]," ++ "[proto-port= |src-port= |" ++ "dst-port=]," + "[ring-size=default:16384]," + "[mbuf-size=default:2176]," + "[total-num-mbufs=default:65535]'\n", +@@ -244,6 +266,64 @@ struct parse_val { + } + + static int ++parse_host(const char *key __rte_unused, const char *value, void *extra_args) ++{ ++ struct pdump_tuples *pt = extra_args; ++ struct in_addr inaddr; ++ struct in6_addr inaddr6; ++ union addr addr; ++ int af = 0; ++ ++ if (inet_pton(AF_INET6, value, &inaddr6) > 0) { ++ af = AF_INET6; ++ addr.in6 = inaddr6; ++ } else if (inet_pton(AF_INET, value, &inaddr) > 0){ ++ af = AF_INET; ++ addr.in = inaddr; ++ } else { ++ printf("IP address invaled\n"); ++ return -EINVAL; ++ } ++ ++ if (pt->filter && pt->filter->af != 0 && af != pt->filter->af) { ++ printf("IPv4 and IPv6 conflict\n"); ++ return -EINVAL; ++ } else { ++ pt->filter->af = af; ++ } ++ ++ if (!strcmp(key, PDUMP_HOST_ARG)) { ++ rte_memcpy(&pt->filter->host_addr, &addr, sizeof(addr)); ++ } else if (!strcmp(key, PDUMP_SRC_ARG)) { ++ rte_memcpy(&pt->filter->s_addr, &addr, sizeof(addr)); ++ } else if (!strcmp(key, PDUMP_DST_ARG)) { ++ rte_memcpy(&pt->filter->d_addr, &addr, sizeof(addr)); ++ } ++ ++ return 0; ++} ++ ++static int ++parse_proto(const char *key __rte_unused, const char *value, void *extra_args) ++{ ++ struct pdump_tuples *pt = extra_args; ++ ++ if (!strcmp(value, "tcp")) { ++ pt->filter->proto = IPPROTO_TCP; ++ } else if (!strcmp(value, "udp")) { ++ pt->filter->proto = IPPROTO_UDP; ++ } else if (!strcmp(value, "icmp")) { ++ pt->filter->proto = IPPROTO_ICMP; ++ } else { ++ printf("invalid value:\"%s\" for key:\"%s\", " ++ "value must be tcp/udp/icmp\n", value, key); ++ return -EINVAL; ++ } ++ ++ return 0; ++} ++ ++static int + parse_pdump(const char *optarg) + { + struct rte_kvargs *kvlist; +@@ -370,6 +450,75 @@ struct parse_val { + } else + pt->total_num_mbufs = MBUFS_PER_POOL; + ++ /* filter parsing and validation */ ++ pt->filter = rte_zmalloc("pdump_filter", ++ sizeof(struct pdump_filter), 0); ++ cnt1 = rte_kvargs_count(kvlist, PDUMP_HOST_ARG); ++ if (cnt1 == 1) { ++ ret = rte_kvargs_process(kvlist, PDUMP_HOST_ARG, ++ &parse_host, pt); ++ if (ret < 0) ++ goto free_kvlist; ++ } ++ ++ cnt1 = rte_kvargs_count(kvlist, PDUMP_SRC_ARG); ++ if (cnt1 == 1) { ++ ret = rte_kvargs_process(kvlist, PDUMP_SRC_ARG, ++ &parse_host, pt); ++ if (ret < 0) ++ goto free_kvlist; ++ } ++ ++ cnt1 = rte_kvargs_count(kvlist, PDUMP_DST_ARG); ++ if (cnt1 == 1) { ++ ret = rte_kvargs_process(kvlist, PDUMP_DST_ARG, ++ &parse_host, pt); ++ if (ret < 0) ++ goto free_kvlist; ++ } ++ ++ ++ cnt1 = rte_kvargs_count(kvlist, PDUMP_PROTO_PORT_AGE); ++ if (cnt1 == 1) { ++ v.min = 1; ++ v.max = UINT16_MAX; ++ ret = rte_kvargs_process(kvlist, PDUMP_PROTO_PORT_AGE, ++ &parse_uint_value, &v); ++ if (ret < 0) ++ goto free_kvlist; ++ pt->filter->proto_port = (uint16_t) v.val; ++ } ++ ++ cnt1 = rte_kvargs_count(kvlist, PDUMP_SPORT_ARG); ++ if (cnt1 == 1) { ++ v.min = 1; ++ v.max = UINT16_MAX; ++ ret = rte_kvargs_process(kvlist, PDUMP_SPORT_ARG, ++ &parse_uint_value, &v); ++ if (ret < 0) ++ goto free_kvlist; ++ pt->filter->s_port = (uint16_t) v.val; ++ } ++ ++ cnt1 = rte_kvargs_count(kvlist, PDUMP_DPORT_ARG); ++ if (cnt1 == 1) { ++ v.min = 1; ++ v.max = UINT16_MAX; ++ ret = rte_kvargs_process(kvlist, PDUMP_DPORT_ARG, ++ &parse_uint_value, &v); ++ if (ret < 0) ++ goto free_kvlist; ++ pt->filter->d_port = (uint16_t) v.val; ++ } ++ ++ cnt1 = rte_kvargs_count(kvlist, PDUMP_PROTO_ARG); ++ if (cnt1 == 1) { ++ ret = rte_kvargs_process(kvlist, PDUMP_PROTO_ARG, ++ &parse_proto, pt); ++ if (ret < 0) ++ goto free_kvlist; ++ } ++ + num_tuples++; + + free_kvlist: +@@ -510,6 +659,8 @@ struct parse_val { + rte_ring_free(pt->rx_ring); + if (pt->tx_ring) + rte_ring_free(pt->tx_ring); ++ if (pt->filter) ++ rte_free(pt->filter); + } + } + +@@ -837,20 +988,20 @@ struct parse_val { + pt->queue, + RTE_PDUMP_FLAG_RX, + pt->rx_ring, +- pt->mp, NULL); ++ pt->mp, pt->filter); + ret1 = rte_pdump_enable_by_deviceid( + pt->device_id, + pt->queue, + RTE_PDUMP_FLAG_TX, + pt->tx_ring, +- pt->mp, NULL); ++ pt->mp, pt->filter); + } else if (pt->dump_by_type == PORT_ID) { + ret = rte_pdump_enable(pt->port, pt->queue, + RTE_PDUMP_FLAG_RX, +- pt->rx_ring, pt->mp, NULL); ++ pt->rx_ring, pt->mp, pt->filter); + ret1 = rte_pdump_enable(pt->port, pt->queue, + RTE_PDUMP_FLAG_TX, +- pt->tx_ring, pt->mp, NULL); ++ pt->tx_ring, pt->mp, pt->filter); + } + } else if (pt->dir == RTE_PDUMP_FLAG_RX) { + if (pt->dump_by_type == DEVICE_ID) +@@ -858,22 +1009,22 @@ struct parse_val { + pt->device_id, + pt->queue, + pt->dir, pt->rx_ring, +- pt->mp, NULL); ++ pt->mp, pt->filter); + else if (pt->dump_by_type == PORT_ID) + ret = rte_pdump_enable(pt->port, pt->queue, + pt->dir, +- pt->rx_ring, pt->mp, NULL); ++ pt->rx_ring, pt->mp, pt->filter); + } else if (pt->dir == RTE_PDUMP_FLAG_TX) { + if (pt->dump_by_type == DEVICE_ID) + ret = rte_pdump_enable_by_deviceid( + pt->device_id, + pt->queue, + pt->dir, +- pt->tx_ring, pt->mp, NULL); ++ pt->tx_ring, pt->mp, pt->filter); + else if (pt->dump_by_type == PORT_ID) + ret = rte_pdump_enable(pt->port, pt->queue, + pt->dir, +- pt->tx_ring, pt->mp, NULL); ++ pt->tx_ring, pt->mp, pt->filter); + } + if (ret < 0 || ret1 < 0) { + cleanup_pdump_resources(); +diff --git a/lib/librte_pdump/rte_pdump.c b/lib/librte_pdump/rte_pdump.c +index 746005a..8a252d5 100644 +--- a/lib/librte_pdump/rte_pdump.c ++++ b/lib/librte_pdump/rte_pdump.c +@@ -9,6 +9,10 @@ + #include + #include + #include ++#include ++#include ++#include ++#include + + #include "rte_pdump.h" + +@@ -69,6 +73,132 @@ struct pdump_response { + } rx_cbs[RTE_MAX_ETHPORTS][RTE_MAX_QUEUES_PER_PORT], + tx_cbs[RTE_MAX_ETHPORTS][RTE_MAX_QUEUES_PER_PORT]; + ++static int ++inet_addr_equal(int af, const union addr *a1, ++ const union addr *a2) ++{ ++ switch (af) { ++ case AF_INET: ++ return a1->in.s_addr == a2->in.s_addr; ++ case AF_INET6: ++ return memcmp(a1->in6.s6_addr, a2->in6.s6_addr, 16) == 0; ++ default: ++ return memcmp(a1, a2, sizeof(union addr)) == 0; ++ } ++} ++ ++static int ++inet_is_addr_any(int af, const union addr *addr) ++{ ++ switch (af) { ++ case AF_INET: ++ return addr->in.s_addr == htonl(INADDR_ANY); ++ case AF_INET6: ++ return IN6_ARE_ADDR_EQUAL(&addr->in6, &in6addr_any); ++ default: ++ return -1; ++ } ++ ++ return -1; ++} ++static int ++pdump_filter(struct rte_mbuf *m, struct pdump_filter *filter) ++{ ++ struct rte_ether_hdr *eth_hdr; ++ struct vlan_eth_hdr *vlan_eth_hdr; ++ union addr s_addr, d_addr; ++ int prepend = 0; ++ uint16_t type = 0; ++ uint16_t iph_len = 0; ++ uint8_t proto = 0; ++ ++ int af; ++ ++ if (filter->af == 0 && filter->s_port == 0 && ++ filter->d_port == 0 && filter->proto == 0 && ++ filter->proto_port == 0) ++ return 0; ++ ++ eth_hdr = rte_pktmbuf_mtod(m, struct rte_ether_hdr *); ++ ++ if (eth_hdr->ether_type == htons(ETH_P_8021Q)) { ++ prepend += sizeof(struct vlan_eth_hdr); ++ vlan_eth_hdr = rte_pktmbuf_mtod(m, struct vlan_eth_hdr *); ++ type = vlan_eth_hdr->h_vlan_encapsulated_proto; ++ } else { ++ prepend += sizeof(struct rte_ether_hdr); ++ eth_hdr = rte_pktmbuf_mtod(m, struct rte_ether_hdr *); ++ type = eth_hdr->ether_type; ++ } ++ ++ if (rte_pktmbuf_adj(m, prepend) == NULL) ++ goto prepend; ++ ++ if (type == rte_cpu_to_be_16(RTE_ETHER_TYPE_ARP)) { ++ struct rte_arp_hdr *arp = rte_pktmbuf_mtod(m, struct rte_arp_hdr *); ++ af = AF_INET; ++ s_addr.in.s_addr = arp->arp_data.arp_sip; ++ d_addr.in.s_addr = arp->arp_data.arp_tip; ++ } else if (type == rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) { ++ struct rte_ipv4_hdr *ip4 = rte_pktmbuf_mtod(m, struct rte_ipv4_hdr *); ++ af = AF_INET; ++ s_addr.in.s_addr = ip4->src_addr; ++ d_addr.in.s_addr = ip4->dst_addr; ++ proto = ip4->next_proto_id; ++ iph_len = (ip4->version_ihl & 0xf) << 2; ++ } else if (type == rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV6)) { ++ struct rte_ipv6_hdr *ip6 = rte_pktmbuf_mtod(m, struct rte_ipv6_hdr *); ++ af = AF_INET6; ++ rte_memcpy(&s_addr.in6, &ip6->src_addr, 16); ++ rte_memcpy(&d_addr.in6, &ip6->dst_addr, 16); ++ proto = ip6->proto; ++ iph_len = sizeof(struct rte_ipv6_hdr); ++ } else { ++ goto prepend; ++ } ++ ++ /*filter*/ ++ if (!inet_is_addr_any(af, &filter->s_addr) && ++ !inet_addr_equal(af, &filter->s_addr, &s_addr)) ++ goto prepend; ++ if (!inet_is_addr_any(af, &filter->d_addr) && ++ !inet_addr_equal(af, &filter->d_addr, &d_addr)) ++ goto prepend; ++ if (!inet_is_addr_any(af, &filter->host_addr) && ++ !inet_addr_equal(af, &filter->host_addr, &s_addr) && ++ !inet_addr_equal(af, &filter->host_addr, &d_addr)) ++ goto prepend; ++ ++ if (filter->proto && filter->proto != proto) ++ goto prepend; ++ ++ if (filter->s_port || filter->d_port || filter->proto_port) { ++ if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) ++ goto prepend; ++ struct rte_udp_hdr _uh; ++ const struct rte_udp_hdr *uh; ++ uh = rte_pktmbuf_read(m, iph_len, sizeof(_uh), &_uh); ++ if (uh == NULL) ++ goto prepend; ++ if (filter->s_port && filter->s_port != rte_cpu_to_be_16(uh->src_port)) ++ goto prepend; ++ ++ if (filter->d_port && filter->d_port != rte_cpu_to_be_16(uh->dst_port)) ++ goto prepend; ++ ++ if (filter->proto_port && ++ filter->proto_port != rte_cpu_to_be_16(uh->src_port) && ++ filter->proto_port != rte_cpu_to_be_16(uh->dst_port)) ++ goto prepend; ++ } ++ ++ rte_pktmbuf_prepend(m, prepend); ++ return 0; ++ ++prepend: ++ rte_pktmbuf_prepend(m, prepend); ++ return -1; ++} + + static inline void + pdump_copy(struct rte_mbuf **pkts, uint16_t nb_pkts, void *user_params) +@@ -86,6 +216,8 @@ struct pdump_response { + ring = cbs->ring; + mp = cbs->mp; + for (i = 0; i < nb_pkts; i++) { ++ if (pdump_filter(pkts[i], cbs->filter) != 0) ++ continue; + p = rte_pktmbuf_copy(pkts[i], mp, 0, UINT32_MAX); + if (p) + dup_bufs[d_pkts++] = p; +@@ -122,7 +254,7 @@ struct pdump_response { + static int + pdump_register_rx_callbacks(uint16_t end_q, uint16_t port, uint16_t queue, + struct rte_ring *ring, struct rte_mempool *mp, +- uint16_t operation) ++ struct pdump_filter *filter, uint16_t operation) + { + uint16_t qid; + struct pdump_rxtx_cbs *cbs = NULL; +@@ -140,6 +272,7 @@ struct pdump_response { + } + cbs->ring = ring; + cbs->mp = mp; ++ cbs->filter = filter; + cbs->cb = rte_eth_add_first_rx_callback(port, qid, + pdump_rx, cbs); + if (cbs->cb == NULL) { +@@ -176,7 +309,7 @@ struct pdump_response { + static int + pdump_register_tx_callbacks(uint16_t end_q, uint16_t port, uint16_t queue, + struct rte_ring *ring, struct rte_mempool *mp, +- uint16_t operation) ++ struct pdump_filter *filter, uint16_t operation) + { + + uint16_t qid; +@@ -195,6 +328,7 @@ struct pdump_response { + } + cbs->ring = ring; + cbs->mp = mp; ++ cbs->filter = filter; + cbs->cb = rte_eth_add_tx_callback(port, qid, pdump_tx, + cbs); + if (cbs->cb == NULL) { +@@ -238,6 +372,7 @@ struct pdump_response { + uint16_t operation; + struct rte_ring *ring; + struct rte_mempool *mp; ++ struct pdump_filter *filter; + + flags = p->flags; + operation = p->op; +@@ -253,6 +388,7 @@ struct pdump_response { + queue = p->data.en_v1.queue; + ring = p->data.en_v1.ring; + mp = p->data.en_v1.mp; ++ filter = p->data.en_v1.filter; + } else { + ret = rte_eth_dev_get_port_by_name(p->data.dis_v1.device, + &port); +@@ -265,6 +401,7 @@ struct pdump_response { + queue = p->data.dis_v1.queue; + ring = p->data.dis_v1.ring; + mp = p->data.dis_v1.mp; ++ filter = p->data.dis_v1.filter; + } + + /* validation if packet capture is for all queues */ +@@ -303,7 +440,7 @@ struct pdump_response { + if (flags & RTE_PDUMP_FLAG_RX) { + end_q = (queue == RTE_PDUMP_ALL_QUEUES) ? nb_rx_q : queue + 1; + ret = pdump_register_rx_callbacks(end_q, port, queue, ring, mp, +- operation); ++ filter, operation); + if (ret < 0) + return ret; + } +@@ -312,7 +449,7 @@ struct pdump_response { + if (flags & RTE_PDUMP_FLAG_TX) { + end_q = (queue == RTE_PDUMP_ALL_QUEUES) ? nb_tx_q : queue + 1; + ret = pdump_register_tx_callbacks(end_q, port, queue, ring, mp, +- operation); ++ filter, operation); + if (ret < 0) + return ret; + } +diff --git a/lib/librte_pdump/rte_pdump.h b/lib/librte_pdump/rte_pdump.h +index 6b00fc1..3986b07 100644 +--- a/lib/librte_pdump/rte_pdump.h ++++ b/lib/librte_pdump/rte_pdump.h +@@ -15,6 +15,8 @@ + #include + #include + #include ++#include ++#include + + #ifdef __cplusplus + extern "C" { +@@ -29,6 +31,31 @@ enum { + RTE_PDUMP_FLAG_RXTX = (RTE_PDUMP_FLAG_RX|RTE_PDUMP_FLAG_TX) + }; + ++union addr { ++ struct in_addr in; ++ struct in6_addr in6; ++}; ++ ++struct pdump_filter { ++ int af; ++ union addr s_addr; ++ union addr d_addr; ++ union addr host_addr; //s_addr or d_addr ++ ++ uint8_t proto; ++ uint16_t proto_port; //s_port or d_port ++ uint16_t s_port; ++ uint16_t d_port; ++}; ++ ++struct vlan_eth_hdr { ++ unsigned char h_dest[ETH_ALEN]; ++ unsigned char h_source[ETH_ALEN]; ++ unsigned short h_vlan_proto; ++ unsigned short h_vlan_TCI; ++ unsigned short h_vlan_encapsulated_proto; ++}; ++ + /** + * Initialize packet capturing handling + * +-- +1.8.3.1 + diff --git a/patch/dpdk-stable-20.11.10/0003-debug-enable-dpdk-eal-memory-debug.patch b/patch/dpdk-stable-20.11.10/0003-debug-enable-dpdk-eal-memory-debug.patch new file mode 100644 index 000000000..77a18998e --- /dev/null +++ b/patch/dpdk-stable-20.11.10/0003-debug-enable-dpdk-eal-memory-debug.patch @@ -0,0 +1,59 @@ +From 3263fcc900f9e97cf777cb1ad2d84408f6fe7bcf Mon Sep 17 00:00:00 2001 +From: huangyichen +Date: Thu, 1 Jul 2021 21:24:47 +0800 +Subject: [PATCH 3/7] debug: enable dpdk eal memory debug + +The patch is used for memory debug. To use the patch, configure meson with option +-Dc_args="-DRTE_MALLOC_DEBUG" when building dpdk. For example, + +meson -Dc_args="-DRTE_MALLOC_DEBUG" -Dbuildtype=debug -Dprefix=$(pwd)/dpdklib dpdkbuild +ninja -C dpdkbuild +--- + lib/librte_eal/common/rte_malloc.c | 4 ++++ + lib/librte_eal/include/rte_malloc.h | 15 +++++++++++++++ + 2 files changed, 19 insertions(+) + +diff --git a/lib/librte_eal/common/rte_malloc.c b/lib/librte_eal/common/rte_malloc.c +index 684af4e..cc7ebb6 100644 +--- a/lib/librte_eal/common/rte_malloc.c ++++ b/lib/librte_eal/common/rte_malloc.c +@@ -30,6 +30,10 @@ + #include "eal_memcfg.h" + #include "eal_private.h" + ++int rte_memmory_ok(void *addr) ++{ ++ return malloc_elem_cookies_ok(RTE_PTR_SUB(addr, MALLOC_ELEM_HEADER_LEN)); ++} + + /* Free the memory space back to heap */ + static void +diff --git a/lib/librte_eal/include/rte_malloc.h b/lib/librte_eal/include/rte_malloc.h +index c8da894..3756d0d 100644 +--- a/lib/librte_eal/include/rte_malloc.h ++++ b/lib/librte_eal/include/rte_malloc.h +@@ -248,6 +248,21 @@ struct rte_malloc_socket_stats { + __rte_alloc_size(2, 3); + + /** ++ * Check the header/tailer cookies of memory pointed to by the provided pointer. ++ * ++ * This pointer must have been returned by a previous call to ++ * rte_malloc(), rte_zmalloc(), rte_calloc() or rte_realloc(). ++ * ++ * @param ptr ++ * The pointer to memory to be checked. ++ * @return ++ * - true if the header/tailer cookies are OK. ++ * - Otherwise, false. ++ */ ++int ++rte_memmory_ok(void *ptr); ++ ++/** + * Frees the memory space pointed to by the provided pointer. + * + * This pointer must have been returned by a previous call to +-- +1.8.3.1 + diff --git a/patch/dpdk-stable-20.11.10/0004-ixgbe_flow-patch-ixgbe-fdir-rte_flow-for-dpvs.patch b/patch/dpdk-stable-20.11.10/0004-ixgbe_flow-patch-ixgbe-fdir-rte_flow-for-dpvs.patch new file mode 100644 index 000000000..136ba76a9 --- /dev/null +++ b/patch/dpdk-stable-20.11.10/0004-ixgbe_flow-patch-ixgbe-fdir-rte_flow-for-dpvs.patch @@ -0,0 +1,256 @@ +From 4b9735e0d479916ec0e7636e5440d4538b349148 Mon Sep 17 00:00:00 2001 +From: huangyichen +Date: Fri, 2 Jul 2021 11:55:47 +0800 +Subject: [PATCH 4/7] ixgbe_flow: patch ixgbe fdir rte_flow for dpvs + +1. Ignore fdir flow rule priority attribute. +2. Use different fdir soft-id for flow rules configured for the same queue. +3. Disable fdir mask settings by rte_flow. +4. Allow IPv6 to pass flow rule ETH item validation. +5. TCP & UDP flow item dest port = 0 is invalid of ixgbe_parse_ntuple_filter() +6. Safe free ixgbe_flow_list item of MARCO RTE_MALLOC_DEBUG is define (configure meson with option -Dc_args="-DRTE_MALLOC_DEBUG") +--- + drivers/net/ixgbe/ixgbe_flow.c | 119 ++++++++++++++++++++++++++++++++++++----- + 1 file changed, 105 insertions(+), 14 deletions(-) + +diff --git a/drivers/net/ixgbe/ixgbe_flow.c b/drivers/net/ixgbe/ixgbe_flow.c +index 7e5b684..e9bc402 100644 +--- a/drivers/net/ixgbe/ixgbe_flow.c ++++ b/drivers/net/ixgbe/ixgbe_flow.c +@@ -2,7 +2,6 @@ + * Copyright(c) 2010-2016 Intel Corporation + */ + +-#include + #include + #include + #include +@@ -15,6 +14,7 @@ + #include + #include + ++#include + #include + #include + #include +@@ -468,6 +468,29 @@ const struct rte_flow_action *next_no_void_action( + } + + tcp_spec = item->spec; ++ /* ++ * DPVS filted by fdir is expected, ++ * With dpvs single worker mode pattern had set: ++ * ----------------------------------------------- ++ * ITEM Spec Mask ++ * ETH NULL NULL ++ * IPV4|6 src_addr 0 0 ++ * dst_addr laddr 0xFFFFFFFF ++ * UDP|TCP src_port 0 0 ++ * dst_port 0 0 ++ * END ++ * ----------------------------------------------- ++ * It should return error here ++ * And continue by ixgbe_parse_fdir_filter() ++ * */ ++ if (tcp_spec->hdr.dst_port == 0 && ++ tcp_mask->hdr.dst_port == 0) { ++ memset(filter, 0, sizeof(struct rte_eth_ntuple_filter)); ++ rte_flow_error_set(error, EINVAL, ++ RTE_FLOW_ERROR_TYPE_ITEM, ++ item, "Not supported by ntuple filter"); ++ return -rte_errno; ++ } + filter->dst_port = tcp_spec->hdr.dst_port; + filter->src_port = tcp_spec->hdr.src_port; + filter->tcp_flags = tcp_spec->hdr.tcp_flags; +@@ -501,6 +524,30 @@ const struct rte_flow_action *next_no_void_action( + filter->src_port_mask = udp_mask->hdr.src_port; + + udp_spec = item->spec; ++ /* ++ * DPVS filted by fdir is expected, ++ * With dpvs single worker mode pattern had set: ++ * ----------------------------------------------- ++ * ITEM Spec Mask ++ * ETH NULL NULL ++ * IPV4|6 src_addr 0 0 ++ * dst_addr laddr 0xFFFFFFFF ++ * UDP|TCP src_port 0 0 ++ * dst_port 0 0 ++ * END ++ * ----------------------------------------------- ++ * It should return error here ++ * And continue by ixgbe_parse_fdir_filter() ++ * */ ++ ++ if (udp_spec->hdr.dst_port == 0 && ++ udp_mask->hdr.dst_port == 0) { ++ memset(filter, 0, sizeof(struct rte_eth_ntuple_filter)); ++ rte_flow_error_set(error, EINVAL, ++ RTE_FLOW_ERROR_TYPE_ITEM, ++ item, "Not supported by ntuple filter"); ++ return -rte_errno; ++ } + filter->dst_port = udp_spec->hdr.dst_port; + filter->src_port = udp_spec->hdr.src_port; + } else if (item->type == RTE_FLOW_ITEM_TYPE_SCTP) { +@@ -1419,11 +1466,8 @@ const struct rte_flow_action *next_no_void_action( + + /* not supported */ + if (attr->priority) { +- memset(rule, 0, sizeof(struct ixgbe_fdir_rule)); +- rte_flow_error_set(error, EINVAL, +- RTE_FLOW_ERROR_TYPE_ATTR_PRIORITY, +- attr, "Not support priority."); +- return -rte_errno; ++ PMD_DRV_LOG(INFO, "ixgbe flow doesn't support priority %d " ++ "(priority must be 0), ignore and continue....\n", attr->priority); + } + + /* check if the first not void action is QUEUE or DROP. */ +@@ -1642,7 +1686,7 @@ static inline uint8_t signature_match(const struct rte_flow_item pattern[]) + * value. So, we need not do anything for the not provided fields later. + */ + memset(rule, 0, sizeof(struct ixgbe_fdir_rule)); +- memset(&rule->mask, 0xFF, sizeof(struct ixgbe_hw_fdir_mask)); ++ memset(&rule->mask, 0, sizeof(struct ixgbe_hw_fdir_mask)); /* mask default zero */ + rule->mask.vlan_tci_mask = 0; + rule->mask.flex_bytes_mask = 0; + +@@ -1760,6 +1804,8 @@ static inline uint8_t signature_match(const struct rte_flow_item pattern[]) + } + } else { + if (item->type != RTE_FLOW_ITEM_TYPE_IPV4 && ++ /* Signature mode supports IPv6. */ ++ item->type != RTE_FLOW_ITEM_TYPE_IPV6 && + item->type != RTE_FLOW_ITEM_TYPE_VLAN) { + memset(rule, 0, sizeof(struct ixgbe_fdir_rule)); + rte_flow_error_set(error, EINVAL, +@@ -1815,6 +1861,10 @@ static inline uint8_t signature_match(const struct rte_flow_item pattern[]) + */ + rule->ixgbe_fdir.formatted.flow_type = + IXGBE_ATR_FLOW_TYPE_IPV4; ++ ++ /* Update flow rule mode by global param. */ ++ rule->mode = dev->data->dev_conf.fdir_conf.mode; ++ + /*Not supported last point for range*/ + if (item->last) { + rte_flow_error_set(error, EINVAL, +@@ -1888,6 +1938,9 @@ static inline uint8_t signature_match(const struct rte_flow_item pattern[]) + rule->ixgbe_fdir.formatted.flow_type = + IXGBE_ATR_FLOW_TYPE_IPV6; + ++ /* Update flow rule mode by global param. */ ++ rule->mode = dev->data->dev_conf.fdir_conf.mode; ++ + /** + * 1. must signature match + * 2. not support last +@@ -2748,12 +2801,45 @@ static inline uint8_t signature_match(const struct rte_flow_item pattern[]) + return ixgbe_parse_fdir_act_attr(attr, actions, rule, error); + } + ++static inline int ++ixgbe_fdir_rule_patch(struct rte_eth_dev *dev, struct ixgbe_fdir_rule *rule) ++{ ++ static uint32_t softid[IXGBE_MAX_RX_QUEUE_NUM] = { 0 }; ++ ++ if (!rule) ++ return 0; ++ ++ if (!dev || !dev->data) ++ return -EINVAL; ++ if (rule->queue >= IXGBE_MAX_RX_QUEUE_NUM) ++ return -EINVAL; ++ ++ /* Soft-id for different rx-queue should be different. */ ++ rule->soft_id = softid[rule->queue]++; ++ ++ /* Disable mask config from rte_flow. ++ * FIXME: ++ * Ixgbe only supports one global mask, all the masks should be the same. ++ * Generally, fdir masks should be configured globally before port start. ++ * But the rte_flow configures masks at flow creation. So we disable fdir ++ * mask configs in rte_flow and configure it globally when port start. ++ * Refer to `ixgbe_dev_start/ixgbe_fdir_configure` for details. The global ++ * masks are configured into device initially with user specified params. ++ */ ++ rule->b_mask = 0; ++ ++ /* Use user-defined mode. */ ++ rule->mode = dev->data->dev_conf.fdir_conf.mode; ++ ++ return 0; ++} ++ + static int + ixgbe_parse_fdir_filter(struct rte_eth_dev *dev, + const struct rte_flow_attr *attr, + const struct rte_flow_item pattern[], + const struct rte_flow_action actions[], +- struct ixgbe_fdir_rule *rule, ++ struct ixgbe_fdir_rule *rule, bool b_patch, + struct rte_flow_error *error) + { + int ret; +@@ -2787,13 +2873,18 @@ static inline uint8_t signature_match(const struct rte_flow_item pattern[]) + rule->ixgbe_fdir.formatted.dst_port != 0)) + return -ENOTSUP; + +- if (fdir_mode == RTE_FDIR_MODE_NONE || +- fdir_mode != rule->mode) ++ if (fdir_mode == RTE_FDIR_MODE_NONE) + return -ENOTSUP; + + if (rule->queue >= dev->data->nb_rx_queues) + return -ENOTSUP; + ++ if (ret) ++ return ret; ++ ++ if (b_patch) ++ return ixgbe_fdir_rule_patch(dev, rule); ++ + return ret; + } + +@@ -3128,7 +3219,7 @@ static inline uint8_t signature_match(const struct rte_flow_item pattern[]) + + memset(&fdir_rule, 0, sizeof(struct ixgbe_fdir_rule)); + ret = ixgbe_parse_fdir_filter(dev, attr, pattern, +- actions, &fdir_rule, error); ++ actions, &fdir_rule, true, error); + if (!ret) { + /* A mask cannot be deleted. */ + if (fdir_rule.b_mask) { +@@ -3299,7 +3390,7 @@ static inline uint8_t signature_match(const struct rte_flow_item pattern[]) + + memset(&fdir_rule, 0, sizeof(struct ixgbe_fdir_rule)); + ret = ixgbe_parse_fdir_filter(dev, attr, pattern, +- actions, &fdir_rule, error); ++ actions, &fdir_rule, false, error); + if (!ret) + return 0; + +@@ -3335,7 +3426,7 @@ static inline uint8_t signature_match(const struct rte_flow_item pattern[]) + struct ixgbe_eth_syn_filter_ele *syn_filter_ptr; + struct ixgbe_eth_l2_tunnel_conf_ele *l2_tn_filter_ptr; + struct ixgbe_fdir_rule_ele *fdir_rule_ptr; +- struct ixgbe_flow_mem *ixgbe_flow_mem_ptr; ++ struct ixgbe_flow_mem *ixgbe_flow_mem_ptr, *next_ptr; + struct ixgbe_hw_fdir_info *fdir_info = + IXGBE_DEV_PRIVATE_TO_FDIR_INFO(dev->data->dev_private); + struct ixgbe_rss_conf_ele *rss_filter_ptr; +@@ -3432,7 +3523,7 @@ static inline uint8_t signature_match(const struct rte_flow_item pattern[]) + return ret; + } + +- TAILQ_FOREACH(ixgbe_flow_mem_ptr, &ixgbe_flow_list, entries) { ++ TAILQ_FOREACH_SAFE(ixgbe_flow_mem_ptr, &ixgbe_flow_list, entries, next_ptr) { + if (ixgbe_flow_mem_ptr->flow == pmd_flow) { + TAILQ_REMOVE(&ixgbe_flow_list, + ixgbe_flow_mem_ptr, entries); +-- +1.8.3.1 + diff --git a/patch/dpdk-stable-20.11.10/0005-bonding-allow-slaves-from-different-numa-nodes.patch b/patch/dpdk-stable-20.11.10/0005-bonding-allow-slaves-from-different-numa-nodes.patch new file mode 100644 index 000000000..12a011e16 --- /dev/null +++ b/patch/dpdk-stable-20.11.10/0005-bonding-allow-slaves-from-different-numa-nodes.patch @@ -0,0 +1,50 @@ +From 30c3918317ea30a7586f2c081a6623c4574dade9 Mon Sep 17 00:00:00 2001 +From: huangyichen +Date: Wed, 4 Aug 2021 15:16:04 +0800 +Subject: [PATCH 5/7] bonding: allow slaves from different numa nodes + +Note the patch may have a negative influnce on performance. +It's not a good practice to bonding slaves across numa nodes. +--- + drivers/net/bonding/rte_eth_bond_pmd.c | 18 ++++++++++++++++-- + 1 file changed, 16 insertions(+), 2 deletions(-) + +diff --git a/drivers/net/bonding/rte_eth_bond_pmd.c b/drivers/net/bonding/rte_eth_bond_pmd.c +index 0c9a1df..371c888 100644 +--- a/drivers/net/bonding/rte_eth_bond_pmd.c ++++ b/drivers/net/bonding/rte_eth_bond_pmd.c +@@ -1780,7 +1780,14 @@ struct bwg_slave { + + errval = rte_eth_rx_queue_setup(slave_eth_dev->data->port_id, q_id, + bd_rx_q->nb_rx_desc, +- rte_eth_dev_socket_id(slave_eth_dev->data->port_id), ++ // In spite of performance problem, bonding slaves had better to support ++ // slaves from different numa nodes. Considering that numa node on which ++ // the resources of bonding port is allocated from is specified by ++ // rte_eth_bond_create() at bonding creation, the slave's queue_setup ++ // would fail if specified with the slave's numa node id that is different ++ // from the one of the bonding port. See rte_eth_dma_zone_reserve() for ++ // details. ++ SOCKET_ID_ANY, + &(bd_rx_q->rx_conf), bd_rx_q->mb_pool); + if (errval != 0) { + RTE_BOND_LOG(ERR, +@@ -1796,7 +1803,14 @@ struct bwg_slave { + + errval = rte_eth_tx_queue_setup(slave_eth_dev->data->port_id, q_id, + bd_tx_q->nb_tx_desc, +- rte_eth_dev_socket_id(slave_eth_dev->data->port_id), ++ // In spite of performance problem, bonding slaves had better to support ++ // slaves from different numa nodes. Considering that numa node on which ++ // the resources of bonding port is allocated from is specified by ++ // rte_eth_bond_create() at bonding creation, the slave's queue_setup ++ // would fail if specified with the slave's numa node id that is different ++ // from the one of the bonding port. See rte_eth_dma_zone_reserve() for ++ // details. ++ SOCKET_ID_ANY, + &bd_tx_q->tx_conf); + if (errval != 0) { + RTE_BOND_LOG(ERR, +-- +1.8.3.1 + diff --git a/patch/dpdk-stable-20.11.10/0006-bonding-fix-problem-in-mode-4-dropping-multicast-pac.patch b/patch/dpdk-stable-20.11.10/0006-bonding-fix-problem-in-mode-4-dropping-multicast-pac.patch new file mode 100644 index 000000000..c7f420f8d --- /dev/null +++ b/patch/dpdk-stable-20.11.10/0006-bonding-fix-problem-in-mode-4-dropping-multicast-pac.patch @@ -0,0 +1,79 @@ +From 2d3c711e48d4f09200096348be1286eec10301f6 Mon Sep 17 00:00:00 2001 +From: yuwenchao +Date: Fri, 2 Aug 2024 13:32:36 +0800 +Subject: [PATCH 6/7] bonding: fix problem in mode 4 dropping multicast packets + +Signed-off-by: yuwenchao +--- + drivers/net/bonding/rte_eth_bond_pmd.c | 38 +++++++++++++++++++++------------- + 1 file changed, 24 insertions(+), 14 deletions(-) + +diff --git a/drivers/net/bonding/rte_eth_bond_pmd.c b/drivers/net/bonding/rte_eth_bond_pmd.c +index 371c888..f770f50 100644 +--- a/drivers/net/bonding/rte_eth_bond_pmd.c ++++ b/drivers/net/bonding/rte_eth_bond_pmd.c +@@ -309,7 +309,6 @@ + + uint8_t collecting; /* current slave collecting status */ + const uint8_t promisc = rte_eth_promiscuous_get(internals->port_id); +- const uint8_t allmulti = rte_eth_allmulticast_get(internals->port_id); + uint8_t subtype; + uint16_t i; + uint16_t j; +@@ -352,20 +351,28 @@ + * - bonding interface is not in promiscuous mode and + * packet address isn't in mac_addrs array: + * - packet is unicast, +- * - packet is multicast and bonding interface +- * is not in allmulti, ++ * ++ * Notes: ++ * Multicast packets, such as OSPF protocol packets, should not ++ * be dropped, instead they must deliver to DPVS application. + */ +- if (unlikely( +- (!dedicated_rxq && +- is_lacp_packets(hdr->ether_type, subtype, +- bufs[j])) || +- !collecting || +- (!promisc && +- !is_bond_mac_addr(&hdr->d_addr, bond_mac, +- BOND_MAX_MAC_ADDRS) && +- (rte_is_unicast_ether_addr(&hdr->d_addr) || +- !allmulti)))) { ++ if (unlikely((is_lacp_packets(hdr->ether_type, subtype, bufs[j])) ++ || !collecting ++ || (!promisc && !is_bond_mac_addr(&hdr->d_addr, bond_mac, ++ BOND_MAX_MAC_ADDRS) ++ && (rte_is_unicast_ether_addr(&hdr->d_addr))))) { + if (hdr->ether_type == ether_type_slow_be) { ++ if (dedicated_rxq) { ++ /* Error! Lacp packets should never appear here if ++ * dedicated queue enabled. This can be caused by ++ * a lack of support for ethertype rte_flow. Just ++ * issue a warning rather than dropping the packets ++ * so that the lacp state machine can work properly. ++ * */ ++ RTE_BOND_LOG(WARNING, "receive lacp packets from queue %d " ++ "of port %d when dedicated queue enabled", ++ bd_rx_q->queue_id, slaves[idx]); ++ } + bond_mode_8023ad_handle_slow_pkt( + internals, slaves[idx], bufs[j]); + } else +@@ -1288,8 +1295,11 @@ struct bwg_slave { + slave_port_ids[i]; + } + +- if (unlikely(dist_slave_count < 1)) ++ if (unlikely(dist_slave_count < 1)) { ++ RTE_BOND_LOG(WARNING, "no distributing slaves on bonding port %d", ++ internals->port_id); + return 0; ++ } + + return tx_burst_balance(queue, bufs, nb_bufs, dist_slave_port_ids, + dist_slave_count); +-- +1.8.3.1 + diff --git a/patch/dpdk-stable-20.11.10/0007-bonding-device-sends-packets-with-user-specified-sal.patch b/patch/dpdk-stable-20.11.10/0007-bonding-device-sends-packets-with-user-specified-sal.patch new file mode 100644 index 000000000..50b5ebf82 --- /dev/null +++ b/patch/dpdk-stable-20.11.10/0007-bonding-device-sends-packets-with-user-specified-sal.patch @@ -0,0 +1,92 @@ +From 69849e246e15f3e202e539809cefd18fe7083575 Mon Sep 17 00:00:00 2001 +From: yuwenchao +Date: Tue, 30 Jul 2024 15:39:28 +0800 +Subject: [PATCH 7/7] bonding device sends packets with user specified salve + port + +The outgoing slave port is specified in mbuf field "hash.txadapter.reserved2". +Support the following 3 bonding mode: +- mode 0: round robin +- mode 2: balance +- mode 4: 8023ad + +Signed-off-by: yuwenchao +--- + drivers/net/bonding/rte_eth_bond_pmd.c | 26 ++++++++++++++++++++++++-- + lib/librte_mbuf/rte_mbuf.h | 2 ++ + 2 files changed, 26 insertions(+), 2 deletions(-) + +diff --git a/drivers/net/bonding/rte_eth_bond_pmd.c b/drivers/net/bonding/rte_eth_bond_pmd.c +index f770f50..3c93365 100644 +--- a/drivers/net/bonding/rte_eth_bond_pmd.c ++++ b/drivers/net/bonding/rte_eth_bond_pmd.c +@@ -587,6 +587,22 @@ struct client_stats_t { + return nb_recv_pkts; + } + ++static inline int ++bond_ethdev_populate_slave_by_user(const struct rte_mbuf *mbuf, const uint16_t *slaves, ++ int num_slave) ++{ ++ uint16_t i, pid = mbuf->hash.txadapter.reserved2; ++ ++ if (likely(pid == RTE_MBUF_PORT_INVALID)) ++ return -1; ++ ++ for (i = 0; i < num_slave; i++) { ++ if (slaves[i] == pid) ++ return i; ++ } ++ return -1; ++} ++ + static uint16_t + bond_ethdev_tx_burst_round_robin(void *queue, struct rte_mbuf **bufs, + uint16_t nb_pkts) +@@ -619,7 +635,9 @@ struct client_stats_t { + + /* Populate slaves mbuf with which packets are to be sent on it */ + for (i = 0; i < nb_pkts; i++) { +- cslave_idx = (slave_idx + i) % num_of_slaves; ++ cslave_idx = bond_ethdev_populate_slave_by_user(bufs[i], slaves, num_of_slaves); ++ if (likely(cslave_idx < 0)) ++ cslave_idx = (slave_idx + i) % num_of_slaves; + slave_bufs[cslave_idx][(slave_nb_pkts[cslave_idx])++] = bufs[i]; + } + +@@ -1176,7 +1194,11 @@ struct bwg_slave { + + for (i = 0; i < nb_bufs; i++) { + /* Populate slave mbuf arrays with mbufs for that slave. */ +- uint16_t slave_idx = bufs_slave_port_idxs[i]; ++ int slave_idx; ++ ++ slave_idx = bond_ethdev_populate_slave_by_user(bufs[i], slave_port_ids, slave_count); ++ if (likely(slave_idx < 0)) ++ slave_idx = bufs_slave_port_idxs[i]; + + slave_bufs[slave_idx][slave_nb_bufs[slave_idx]++] = bufs[i]; + } +diff --git a/lib/librte_mbuf/rte_mbuf.h b/lib/librte_mbuf/rte_mbuf.h +index d079462..d6072ea 100644 +--- a/lib/librte_mbuf/rte_mbuf.h ++++ b/lib/librte_mbuf/rte_mbuf.h +@@ -589,6 +589,7 @@ static inline struct rte_mbuf *rte_mbuf_raw_alloc(struct rte_mempool *mp) + + if (rte_mempool_get(mp, (void **)&m) < 0) + return NULL; ++ m->hash.txadapter.reserved2 = RTE_MBUF_PORT_INVALID; + __rte_mbuf_raw_sanity_check(m); + return m; + } +@@ -864,6 +865,7 @@ static inline void rte_pktmbuf_reset(struct rte_mbuf *m) + m->vlan_tci_outer = 0; + m->nb_segs = 1; + m->port = RTE_MBUF_PORT_INVALID; ++ m->hash.txadapter.reserved2 = RTE_MBUF_PORT_INVALID; + + m->ol_flags &= EXT_ATTACHED_MBUF; + m->packet_type = 0; +-- +1.8.3.1 + diff --git a/scripts/dpdk-build.sh b/scripts/dpdk-build.sh index 8416120b0..3e2d1a4b6 100755 --- a/scripts/dpdk-build.sh +++ b/scripts/dpdk-build.sh @@ -5,7 +5,7 @@ build_options="-Denable_kmods=true" debug_options="-Dbuildtype=debug -Dc_args=-DRTE_MALLOC_DEBUG" -dpdkver=20.11.1 # default dpdk version (use stable version) +dpdkver=20.11.10 # default dpdk version (use stable version) tarball=dpdk-${dpdkver}.tar.xz srcdir=dpdk-stable-$dpdkver @@ -23,29 +23,16 @@ function help() echo -e "\033[31m -p specify the dpdk patch directory, default $(pwd)/patch/dpdk-stable-$dpdkver\033[0m" } -function getfullpath() -{ - local dir=$(dirname $1) - local base=$(basename $1) - if test -d ${dir}; then - pushd ${dir} >/dev/null 2>&1 - echo ${PWD}/${base} - popd >/dev/null 2>&1 - return 0 - fi - return 1 -} - function set_work_directory() { [ ! -d $1 ] && return 1 - workdir=$(getfullpath $1)/dpdk + workdir=$(realpath $1)/dpdk } function set_patch_directory() { [ ! -d $1 ] && return 1 - patchdir=$(getfullpath $1) + patchdir=$(realpath $1) } ## parse args From 7b4124c9c24566d2b0dad2a075c1450d6e621fb1 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Thu, 1 Aug 2024 13:53:11 +0800 Subject: [PATCH 60/63] build: fix build warnings issued by high version gcc Signed-off-by: ywc689 --- include/sctp/sctp.h | 0 src/dpdk.mk | 17 +++++++---------- src/inetaddr.c | 2 +- tools/dpvs-agent/cmd/dpvs-agent-server/Makefile | 2 +- tools/healthcheck/Makefile | 2 +- tools/ipvsadm/ipvsadm.c | 8 +++++--- tools/keepalived/keepalived/include/layer4.h | 1 + tools/keepalived/keepalived/vrrp/vrrp_daemon.c | 1 + 8 files changed, 17 insertions(+), 16 deletions(-) mode change 100755 => 100644 include/sctp/sctp.h diff --git a/include/sctp/sctp.h b/include/sctp/sctp.h old mode 100755 new mode 100644 diff --git a/src/dpdk.mk b/src/dpdk.mk index e3c4c2759..81ef04305 100644 --- a/src/dpdk.mk +++ b/src/dpdk.mk @@ -26,24 +26,21 @@ If dpdk has installed already, please ensure the libdpdk.pc file could be found You may fix the problem by setting LIBDPDKPC_PATH (in file src/dpdk.mk) to the path of libdpdk.pc file explicitly endef -# It's noted that pkg-config version 0.29.2 is recommended, +# It's noted that pkg-config version 0.29.2+ is recommended, # pkg-config 0.27.1 would mess up the ld flags when linking dpvs. -PKGCONFIG_VERSION=$(shell pkg-config pkg-config --version) -ifneq "v$(PKGCONFIG_VERSION)" "v0.29.2" -$(warning "The pkg-config version is $(PKGCONFIG_VERSION) but 0.29.2 is recommended.") +PKGCONFIG_VERSION=$(shell pkg-config --version) ifeq "v$(PKGCONFIG_VERSION)" "v0.27.1" -$(error "pkg-config version $(PKGCONFIG_VERSION) isn't supported by dpvs, please use 0.29.2 instead.") -endif +$(error "pkg-config version $(PKGCONFIG_VERSION) isn't supported, require 0.29.2+") endif -ifeq ($(shell pkg-config --exists libdpdk && echo 0),0) -CFLAGS += -DALLOW_EXPERIMENTAL_API $(shell pkg-config --cflags libdpdk) -LIBS += $(shell pkg-config --static --libs libdpdk) -else ifneq ($(wildcard $(LIBDPDKPC_PATH)),) CFLAGS += -DALLOW_EXPERIMENTAL_API $(shell PKG_CONFIG_PATH=$(LIBDPDKPC_PATH) pkg-config --cflags libdpdk) LIBS += $(shell PKG_CONFIG_PATH=$(LIBDPDKPC_PATH) pkg-config --static --libs libdpdk) else +ifeq ($(shell pkg-config --exists libdpdk && echo 0),0) +CFLAGS += -DALLOW_EXPERIMENTAL_API $(shell pkg-config --cflags libdpdk) +LIBS += $(shell pkg-config --static --libs libdpdk) +else $(error $(PKG_CONFIG_ERR_MSG)) endif endif diff --git a/src/inetaddr.c b/src/inetaddr.c index fb833436a..87c1985f0 100644 --- a/src/inetaddr.c +++ b/src/inetaddr.c @@ -314,7 +314,7 @@ static int inet6_addr_gen_stable(struct in6_addr secret, struct inet_device *ide uint32_t prefix[2]; struct rte_ether_addr hwaddr; uint8_t dad_count; - } __rte_packed data; + } data; uint8_t dad_count = 0; memset(&data, 0, sizeof(data)); diff --git a/tools/dpvs-agent/cmd/dpvs-agent-server/Makefile b/tools/dpvs-agent/cmd/dpvs-agent-server/Makefile index 84f6c3105..e5cd00eef 100644 --- a/tools/dpvs-agent/cmd/dpvs-agent-server/Makefile +++ b/tools/dpvs-agent/cmd/dpvs-agent-server/Makefile @@ -1,7 +1,7 @@ TARGET := dpvs-agent GO ?= go -LD_FLAGS = -ldflags="-s -w" +LD_FLAGS = -ldflags="-s -w -X main.buildVersion=$(git rev-parse --short HEAD)" GO_BUILD = CGO_ENABLED=0 $(GO) build $(LD_FLAGS) GO_CLEAN = $(GO) clean INSTALL = install diff --git a/tools/healthcheck/Makefile b/tools/healthcheck/Makefile index 6e866e140..fe4d9400a 100644 --- a/tools/healthcheck/Makefile +++ b/tools/healthcheck/Makefile @@ -1,7 +1,7 @@ TARGET := healthcheck GO ?= go -LD_FLAGS = -ldflags="-s -w" +LD_FLAGS = -ldflags="-s -w -X main.buildVersion=$(git rev-parse --short HEAD)" GO_BUILD = CGO_ENABLED=0 $(GO) build $(LD_FLAGS) GO_CLEAN = $(GO) clean diff --git a/tools/ipvsadm/ipvsadm.c b/tools/ipvsadm/ipvsadm.c index 4938cedd0..f84a4ef0d 100644 --- a/tools/ipvsadm/ipvsadm.c +++ b/tools/ipvsadm/ipvsadm.c @@ -474,7 +474,7 @@ parse_options(int argc, char **argv, struct ipvs_command_entry *ce, int c, parse; poptContext context; char *optarg= NULL; - int intarg = NULL; + int intarg = 0; struct poptOption options_table[] = { { "add-service", 'A', POPT_ARG_NONE, NULL, 'A', NULL, NULL }, { "edit-service", 'E', POPT_ARG_NONE, NULL, 'E', NULL, NULL }, @@ -1566,9 +1566,11 @@ static int parse_match_snat(const char *buf, dpvs_service_compat_t *dpvs_svc) dpvs_svc->af = ip_af; } } else if (strcmp(key, "iif") == 0) { - snprintf(dpvs_svc->match.iifname, sizeof(dpvs_svc->match.iifname), "%s", val); + strncpy(dpvs_svc->match.iifname, val, sizeof(dpvs_svc->match.iifname) - 1); + dpvs_svc->match.iifname[sizeof(dpvs_svc->match.iifname) - 1] = '\0'; } else if (strcmp(key, "oif") == 0) { - snprintf(dpvs_svc->match.oifname, sizeof(dpvs_svc->match.oifname), "%s", val); + strncpy(dpvs_svc->match.oifname, val, sizeof(dpvs_svc->match.oifname) - 1); + dpvs_svc->match.oifname[sizeof(dpvs_svc->match.oifname) - 1] = '\0'; } else { return -1; } diff --git a/tools/keepalived/keepalived/include/layer4.h b/tools/keepalived/keepalived/include/layer4.h index 6f47bb8c2..db399ef4c 100644 --- a/tools/keepalived/keepalived/include/layer4.h +++ b/tools/keepalived/keepalived/include/layer4.h @@ -55,6 +55,7 @@ typedef struct _conn_opts { /* Prototypes defs */ #ifdef _WITH_LVS_ +extern void set_buf(char *buf, size_t buf_len); extern enum connect_result socket_bind_connect(int, conn_opts_t *); #endif diff --git a/tools/keepalived/keepalived/vrrp/vrrp_daemon.c b/tools/keepalived/keepalived/vrrp/vrrp_daemon.c index ea19cc6d3..3998461d0 100644 --- a/tools/keepalived/keepalived/vrrp/vrrp_daemon.c +++ b/tools/keepalived/keepalived/vrrp/vrrp_daemon.c @@ -21,6 +21,7 @@ */ #include "config.h" +#include "sockopt.h" #ifdef _HAVE_SCHED_RT_ #include From b21a018db683120171692640be90a52de218e10d Mon Sep 17 00:00:00 2001 From: ywc689 Date: Fri, 13 Sep 2024 17:50:05 +0800 Subject: [PATCH 61/63] doc: update README.md and version file Signed-off-by: ywc689 --- README.md | 139 ++++++++---- pic/dpvs.drawio | 580 ++++++++++++++++++++++++++++++++++++++++++++++++ pic/dpvs.png | Bin 109765 -> 351633 bytes pic/dpvs.svg | 4 + pic/modules.png | Bin 50640 -> 252711 bytes pic/modules.svg | 4 + src/VERSION | 32 ++- 7 files changed, 705 insertions(+), 54 deletions(-) create mode 100644 pic/dpvs.drawio create mode 100644 pic/dpvs.svg create mode 100644 pic/modules.svg diff --git a/README.md b/README.md index 138aa62a9..b3f678a5a 100644 --- a/README.md +++ b/README.md @@ -23,38 +23,45 @@ Several techniques are applied for high performance: Major features of `DPVS` including: -* *L4 Load Balancer*, including FNAT, DR, Tunnel, DNAT modes, etc. +* *L4 Load Balancer*, supports FNAT, DR, Tunnel and DNAT reverse proxy modes. +* *NAT64* mode for IPv6 quick adaption without changing backend server. * *SNAT* mode for Internet access from internal network. -* *NAT64* forwarding in FNAT mode for quick IPv6 adaptation without application changes. -* Different *schedule algorithms* like RR, WLC, WRR, MH(Maglev Hashing), Conhash(Consistent Hashing) etc. -* User-space *Lite IP stack* (IPv4/IPv6, Routing, ARP, Neighbor, ICMP ...). -* Support *KNI*, *VLAN*, *Bonding*, *Tunneling* for different IDC environment. -* Security aspect, support *TCP syn-proxy*, *Conn-Limit*, *black-list*, *white-list*. -* QoS: *Traffic Control*. +* Adequate *schedule algorithms* like RR, WLC, WRR, MH(Maglev Hash), Conhash(Consistent Hash), etc. +* User-space *lite network stack*: IPv4, IPv6, Routing, ARP, Neighbor, ICMP, LLDP, IPset, etc. +* Support *KNI*, *VLAN*, *Bonding*, *IP Tunnel* for different IDC environment. +* Security aspects support *TCP SYN-proxy*, *Allow/Deny ACL*. +* QoS features such as *Traffic Control*, *Concurrent Connection Limit*. +* Versatile tools, services can be configured with `dpip` `ipvsadm` command line tools, or from config files of `keepalived`, or via restful API provided by `dpvs-agent`. -`DPVS` feature modules are illustrated as following picture. +DPVS consists of the modules illustrated in the diagram below. -![modules](./pic/modules.png) +![modules](./pic/modules.svg) # Quick Start ## Test Environment -This *quick start* is tested with the environment below. +This *quick start* is performed in the environments described below. -* Linux Distribution: CentOS 7.2 -* Kernel: 3.10.0-327.el7.x86_64 -* CPU: Intel(R) Xeon(R) CPU E5-2650 v3 @ 2.30GHz +* Linux Distribution: CentOS 7.6 +* Kernel: 3.10.0-957.el7.x86_64 +* CPU: Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz * NIC: Intel Corporation Ethernet Controller 10-Gigabit X540-AT2 (rev 03) * Memory: 64G with two NUMA node. -* GCC: gcc version 4.8.5 20150623 (Red Hat 4.8.5-4) +* GCC: 4.8.5 20150623 (Red Hat 4.8.5-36) +* Golang: go1.20.4 linux/amd64 (required only when CONFIG_DPVS_AGENT enabled). -Other environments should also be OK if DPDK works, please check [dpdk.org](http://www.dpdk.org) for more info. +Other environments should also be OK if DPDK works, please check [dpdk.org](http://www.dpdk.org) for more information. -* Please check this link for NICs supported by DPDK: http://dpdk.org/doc/nics. -* Note `flow control` ([rte_flow](http://dpdk.org/doc/guides/nics/overview.html#id1)) is needed for `FNAT` and `SNAT` mode with multi-cores. - -> Notes: To let dpvs work properly with multi-cores, rte_flow items must support "ipv4, ipv6, tcp, udp" four items, and rte_flow actions must support "drop, queue" at least. +> Notes: +> 1. Please check this link for NICs supported by DPDK: http://dpdk.org/doc/nics. +> 2. `Flow Control` ([rte_flow](http://dpdk.org/doc/guides/nics/overview.html#id1)) is required for `FNAT` and `SNAT` mode when DPVS running on multi-cores unless `conn redirect` is enabled. The minimum requirements to ensure DPVS works with multi-core properly is that `rte_flow` must support "ipv4, ipv6, tcp, udp" four items, and "drop, queue" two actions. +> 3. DPVS doesn't confine itself to the this test environments. In fact, DPVS is an user-space application which relies very little on operating system, kernel versions, compilers, and other platform discrepancies. As far as is known, DPVS has been verified at least in the following environments. +> * Centos 7.2, 7.6, 7.9 +> * Anolis 8.6, 8.8, 8.9 +> * GCC 4.8, 8.5 +> * Kernel: 3.10.0, 4.18.0, 5.10.134 +> * NIC: Intel IXGBE, NVIDIA MLX5 ## Clone DPVS @@ -65,40 +72,40 @@ $ cd dpvs Well, let's start from DPDK then. -## DPDK setup. +## DPDK setup -Currently, `dpdk-stable-20.11.1` is recommended for `DPVS`, and we will not support dpdk version earlier than dpdk-20.11 any more. If you are still using earlier dpdk versions, such as `dpdk-stable-17.11.2`, `dpdk-stable-17.11.6` and `dpdk-stable-18.11.2`, please use earlier dpvs releases, such as [v1.8.10](https://github.com/iqiyi/dpvs/releases/tag/v1.8.10). +Currently, `dpdk-stable-20.11.10` is recommended for `DPVS`, and we will not support dpdk version earlier than dpdk-20.11 any more. If you are still using earlier dpdk versions, such as `dpdk-stable-17.11.6` and `dpdk-stable-18.11.2`, please use earlier DPVS releases, such as [v1.8.12](https://github.com/iqiyi/dpvs/releases/tag/v1.8.12). > Notes: You can skip this section if experienced with DPDK, and refer the [link](http://dpdk.org/doc/guides/linux_gsg/index.html) for details. ```bash -$ wget https://fast.dpdk.org/rel/dpdk-20.11.1.tar.xz # download from dpdk.org if link failed. -$ tar xf dpdk-20.11.1.tar.xz +$ wget https://fast.dpdk.org/rel/dpdk-20.11.10.tar.xz # download from dpdk.org if link failed. +$ tar xf dpdk-20.11.10.tar.xz ``` ### DPDK patchs There are some patches for DPDK to support extra features needed by DPVS. Apply them if needed. For example, there's a patch for DPDK `kni` driver for hardware multicast, apply it if you are to launch `ospfd` on `kni` device. -> Notes: Assuming we are in DPVS root directory and dpdk-stable-20.11.1 is under it, please note it's not mandatory, just for convenience. +> Notes: It's assumed we are in DPVS root directory where you have installed dpdk-stable-20.11.10 source codes. Please note it's not mandatory, just for convenience. ``` $ cd -$ cp patch/dpdk-stable-20.11.1/*.patch dpdk-stable-20.11.1/ -$ cd dpdk-stable-20.11.1/ +$ cp patch/dpdk-stable-20.11.10/*.patch dpdk-stable-20.11.10/ +$ cd dpdk-stable-20.11.10/ $ patch -p1 < 0001-kni-use-netlink-event-for-multicast-driver-part.patch $ patch -p1 < 0002-pdump-change-dpdk-pdump-tool-for-dpvs.patch $ ... ``` > Tips: It's advised to patch all if your are not sure about what they are meant for. - + ### DPDK build and install -Use meson-ninja to build DPDK libraries, and export environment variable `PKG_CONFIG_PATH` for DPDK app (DPVS). The `dpdk.mk` in DPVS checks the presence of libdpdk. +Use meson-ninja to build DPDK, and export environment variable `PKG_CONFIG_PATH` for DPDK application (DPVS). The sub-Makefile `src/dpdk.mk` in DPVS will check the presence of libdpdk. ```bash -$ cd dpdk-stable-20.11.1 +$ cd dpdk-stable-20.11.10 $ mkdir dpdklib # user desired install folder $ mkdir dpdkbuild # user desired build folder $ meson -Denable_kmods=true -Dprefix=dpdklib dpdkbuild @@ -115,31 +122,49 @@ Next is to set up DPDK hugepage. Our test environment is NUMA system. For single $ # for NUMA machine $ echo 8192 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages $ echo 8192 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages +``` +By default, hugetlbfs is mounted at `/dev/hugepages`, as shown below. +```bash +$ mount | grep hugetlbfs +hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime) +``` +If it's not your case, you should mount hugetlbfs by yourself. + +```bash $ mkdir /mnt/huge $ mount -t hugetlbfs nodev /mnt/huge ``` -Install kernel modules and bind NIC with `uio_pci_generic` driver. Quick start uses only one NIC, normally we use two for FNAT cluster, even four for bonding mode. For example, suppose the NIC we would use to run DPVS is eth0, in the meantime, we still keep another standalone NIC eth1 for debugging. +> Notes: +> 1. Hugepages of other size, such as 1GB-size hugepages, can also be used if your system supports. +> 2. It's recommended to reserve hugepage memory and isolate CPUs used by DPVS with linux kernel cmdline options in production environments, for example `isolcpus=1-9 default_hugepagesz=1G hugepagesz=1G hugepages=32`. + +Next, install kernel modules required by DPDK and DPVS. + +* DPDK driver kernel module: +Depending on your NIC and system, NIC may require binding a DPDK-compitable driver, such as `vfio-pci`, `igb_uio`, or `uio_pci_generic`. Refer to [DPDK doc](https://doc.dpdk.org/guides/linux_gsg/linux_drivers.html) for more details. In this test, we use the linux standard UIO kernel module `uio_pci_generic`. + +* KNI kernel module: +KNI kernel module `rte_kni.ko` is required by DPVS as the exception data path which processes packets not dealt with in DPVS to kernel stack. ```bash $ modprobe uio_pci_generic -$ cd dpdk-stable-20.11.1 +$ cd dpdk-stable-20.11.10 $ insmod dpdkbuild/kernel/linux/kni/rte_kni.ko carrier=on +$ # bind eth0 to uio_pci_generic (Be aware: Network on eth0 will get broken!) $ ./usertools/dpdk-devbind.py --status -$ ifconfig eth0 down # assuming eth0 is 0000:06:00.0 +$ ifconfig eth0 down # assuming eth0's pci-bus location is 0000:06:00.0 $ ./usertools/dpdk-devbind.py -b uio_pci_generic 0000:06:00.0 ``` - > Notes: -> 1. An alternative to the `uio_pci_generic` is `igb_uio`, which is moved to a separated repository [dpdk-kmods](http://git.dpdk.org/dpdk-kmods). -> 2. A kernel module parameter `carrier` is added to `rte_kni.ko` since [DPDK v18.11](https://elixir.bootlin.com/dpdk/v18.11/source/kernel/linux/kni/kni_misc.c), and the default value for it is "off". We need to load `rte_kni.ko` with the extra parameter `carrier=on` to make KNI devices work properly. - -`dpdk-devbind.py -u` can be used to unbind driver and switch it back to Linux driver like `ixgbe`. You can also use `lspci` or `ethtool -i eth0` to check the NIC PCI bus-id. Please refer to [DPDK site](http://www.dpdk.org) for more details. - -> Notes: PMD of Mellanox NIC is built on top of libibverbs using the Raw Ethernet Accelerated Verbs AP. It doesn't rely on UIO/VFIO driver. Thus, Mellanox NICs should not bind the `igb_uio` driver. Refer to [Mellanox DPDK](https://community.mellanox.com/s/article/mellanox-dpdk) for details. +> 1. The test in our Quick Start uses only one NIC. Bind as many NICs as required in your DPVS application to DPDK driver kernel module. For example, you should bind at least 2 NICs if you are testing DPVS with two-arm. +> 2. `dpdk-devbind.py -u` can be used to unbind driver and switch it back to Linux driver like `ixgbe`. Use `lspci` or `ethtool -i eth0` to check the NIC's PCI bus-id. Please refer to [DPDK Doc:Binding and Unbinding Network Ports to/from the Kernel Modules](https://doc.dpdk.org/guides/linux_gsg/linux_drivers.html#binding-and-unbinding-network-ports-to-from-the-kernel-modules) for more details. +> 3. NVIDIA/Mellanox NIC uses bifurcated driver which doesn't rely on UIO/VFIO driver, so not bind any DPDK driver kernel module, but [NVIDIA MLNX_OFED/EN](https://network.nvidia.com/products/infiniband-drivers/linux/mlnx_ofed/) is required. Refer to [Mellanox DPDK](https://enterprise-support.nvidia.com/s/article/mellanox-dpdk) for its PMD and [Compilation Prerequisites](https://doc.dpdk.org/guides/platform/mlx5.html#linux-prerequisites) for OFED installation. +> 4. A kernel module parameter `carrier` has been added to `rte_kni.ko` since [DPDK v18.11](https://elixir.bootlin.com/dpdk/v18.11/source/kernel/linux/kni/kni_misc.c), and the default value for it is "off". We need to load `rte_kni.ko` with extra parameter `carrier=on` to make KNI devices work properly. +> 5. Multiple DPVS instances can run on a single server if there are enough NICs or VFs within one NIC. Refer to [tutorial:Multiple Instances](https://github.com/iqiyi/dpvs/blob/devel/doc/tutorial.md#multi-instance) for details. ## Build DPVS @@ -153,19 +178,21 @@ $ make # or "make -j" to speed up $ make install ``` > Notes: -> 1. Build dependencies may be needed, such as `pkg-config`(version 0.29.2+),`automake`, `libnl3`, `libnl-genl-3.0`, `openssl`, `popt` and `numactl`. You can install the missing dependencies by using the package manager of the system, e.g., `yum install popt-devel automake` (CentOS) or `apt install libpopt-dev autoconfig` (Ubuntu). -> 2. Early `pkg-config` versions (v0.29.2 before) may cause dpvs build failure. If so, please upgrade this tool. +> 1. Build dependencies may be needed, such as `pkg-config`(version 0.29.2+, `automake`, `libnl3`, `libnl-genl-3.0`, `openssl`, `popt` and `numactl`. You can install the missing dependencies with package manager of your system, e.g., `yum install popt-devel automake` (CentOS) or `apt install libpopt-dev autoconfig` (Ubuntu). +> 2. Early `pkg-config` versions (v0.29.2 before) may cause dpvs build failure. If so, please upgrade this tool. Specially, you may upgrade the `pkg-config` on Centos7 to meet the version requirement. +> 3. If you want to compile `dpvs-agent` and `healthcheck`, enable `CONFIG_DPVS_AGENT` in config.mk, and install Golang build environments(Refer to [go.mod](tools/dpvs-agent/go.mod) file for required Golang version). -Output files are installed to `dpvs/bin`. +Output binary files are installed to `dpvs/bin`. ```bash $ ls bin/ -dpip dpvs ipvsadm keepalived +dpip dpvs dpvs-agent healthcheck ipvsadm keepalived ``` * `dpvs` is the main program. -* `dpip` is the tool to set IP address, route, vlan, neigh, etc. +* `dpip` is the tool to manage IP address, route, vlan, neigh, etc. * `ipvsadm` and `keepalived` come from LVS, both are modified. +* `dpvs-agent` and `healthcheck` are alternatives to `keepalived` powered with HTTP API developed with Golang. ## Launch DPVS @@ -180,7 +207,13 @@ and start DPVS, ```bash $ cd /bin $ ./dpvs & + +$ # alternatively and strongly advised, start DPVS with NIC and CPU explicitly specified: +$ ./dpvs -- -a 0000:06:00.0 -l 1-9 ``` +> Notes: +> 1. Run `./dpvs --help` for DPVS supported command line options, and `./dpvs -- --help` for common DPDK EAL command line options. +> 2. The default `dpvs.conf` require 9 CPUs(1 master worker, 8 slave workers), modify it if not so many available CPUs in your system. Check if it's get started ? @@ -198,9 +231,9 @@ If you see this message. Well done, `DPVS` is working with NIC `dpdk0`! EAL: Error - exiting with code: 1 Cause: ports in DPDK RTE (2) != ports in dpvs.conf(1) ``` ->It means the NIC count of DPVS does not match `/etc/dpvs.conf`. Please use `dpdk-devbind` to adjust the NIC number or modify `dpvs.conf`. We'll improve this part to make DPVS more "clever" to avoid modify config file when NIC count does not match. +>It means the number of NIC recognized by DPVS mismatched `/etc/dpvs.conf`. Please either modify NIC number in `dpvs.conf` or specify NICs with EAL option `-a` explicitly. -What config items does `dpvs.conf` support? How to configure them? Well, `DPVS` maintains a config item file `conf/dpvs.conf.items` which lists all supported config entries and corresponding feasible values. Besides, some config sample files maintained as `./conf/dpvs.*.sample` show the configurations of dpvs in some specified cases. +What config items does `dpvs.conf` support? How to configure them? Well, `DPVS` maintains a config item file `conf/dpvs.conf.items` which lists all supported config entries, default values, and feasible value ranges. Besides, some sample config files maintained in `./conf/dpvs.*.sample` gives practical configurations of DPVS in corresponding circumstances. ## Test Full-NAT (FNAT) Load Balancer @@ -234,22 +267,30 @@ Your ip:port : 192.168.100.3:56890 ## Tutorial Docs -More configure examples can be found in the [Tutorial Document](./doc/tutorial.md). Including, +More examples can be found in the [Tutorial Document](./doc/tutorial.md). Including, * WAN-to-LAN `FNAT` reverse proxy. * Direct Route (`DR`) mode setup. * Master/Backup model (`keepalived`) setup. * OSPF/ECMP cluster model setup. * `SNAT` mode for Internet access from internal network. -* Virtual Devices (`Bonding`, `VLAN`, `kni`, `ipip`/`GRE`). +* Virtual Devices (`Bonding`, `VLAN`, `kni`, `ipip`/`GRE` tunnel). * `UOA` module to get real UDP client IP/port in `FNAT`. * ... and more ... We also listed some frequently asked questions in the [FAQ Document](./doc/faq.md). It may help when you run into problems with DPVS. +Browse the [doc](./doc) directory for other documentations, including: +* [IPset](./doc/IPset.md) +* [Traffic Control (TC)](./doc/tc.md) +* [Performance tune](./doc/Worker-Performance-Tuning.md) +* [Backend healthcheck without keepalived](./doc/dest-check.md) +* [Client address conservation in Fullnat](./doc/client-address-conservation-in-fullnat.md) +* [Advices to build and run DPVS in container](./doc/containerized/README.md) + # Performance Test -Our test shows the forwarding speed (pps) of DPVS is several times than LVS and as good as Google's [Maglev](https://research.google.com/pubs/pub44824.html). +Our test shows the forwarding speed (PPS/packets per second) of DPVS is several times than LVS and as good as Google's [Maglev](https://research.google.com/pubs/pub44824.html). ![performance](./pic/performance.png) diff --git a/pic/dpvs.drawio b/pic/dpvs.drawio new file mode 100644 index 000000000..9fa069975 --- /dev/null +++ b/pic/dpvs.drawio @@ -0,0 +1,580 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pic/dpvs.png b/pic/dpvs.png index 25356a44e6b40bb999b3bfee51347d2ddec4c3fb..ecf6c4217ab2971fb50e44accc8f1ab659a80aca 100644 GIT binary patch literal 351633 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�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�EOUk-&gRWyk~t>4Jg^ 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

taIf=p%;6GPWfljlKi9#Nl`(rd!<$`6yX5F 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 SYew%~ literal 109765 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


o+v&1)e@&*rafg%uP3%ufZkI?X`a=k`I;uPr2h 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
*q-_0oMiX@s`Qe=)v&Dq==jte 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�-wW_U+sEmRoM|ML-G_ z#O+sBbspXO?ffP`(tOKl;tpc+$~vh46`%qkRN%nZgZE7Dyr%2z%b%~6>E8aYP9EH` zxW%vWQb0sBd(y_clCWJ?69x{J3WQ66?c2B4t5*-!OC0yyb5EZ>eL#!=twl!YT_~T(f3P5W4T!vE$lnuZ3kB4Z-%?yLWFi9ck%A=W!2w^wCF6nl!<0KNnI^2kOA* zLVQI~hiK`5?|kk1{Lr@(tB>wR95lQ#_RJf;almuO4_4%TffBa!O2h(GfC>btK(AiC zV4f9wk8i&DW_S*FNgvp+8^r_rv|o7P1=zGP9Bekm(6AjnIQc&O@Iy_#8*aGa>8GED zXwg*n zv}W_5)`V)p0~cAin8NRY*nSV}U$(ACDiE6Z*)qCo0k1;Bb^)UC6e>Ukd{f})(WB2i z^9+bl*nx#H6NfN&N9#bhF$9>J#V>#O!yg`g_~D9*{HvY?t9k6$vFDz9E*|QMOK%Xc zMT-_yRaFJ+qn${hylYtIH6ThkkQ*L@_(hXq-TN(blkCqw=I1Hsq7K56PaUWOzaP@y ziBbQaMEnlzdw$jHjS6@b61EEvji*onD&VIAhYuhA```Z#GbXk-gSAAeCy}lxT8HR1 ztn?ROd@<0mLx&D~_U!RfDOFuqt$6p{ck9-zEB0?kS3Rxg5D<3)z`IkYPW$%l^GiSq z<(HDIM^gY3FT!>VCeu-e0rp=!lGK4Z1hL4qZ}GU5oo?CmX~(K{iwk@G5w;7{jb~8- zD&VsM>({UE(W3_(umv$j^g!nk?5%U>&MQ`|@L3?+>NalN2#5SejT(VbBPyNfHv0d^ zKmHMmFRNCq3PL~%6@;p+lL}A)DnJFO02NRw@c#$uhaRy&b59)r0000 + + +
FWD
FWD
FWD
FWD
FWD
FWD
FWD
FWD
Lightweight IP Stack
Lightweight IP Stack
1
1
2
2
3
3
4
4
0
0
Control
Plane
Control...
UIO, VFIO, Bifurcated Driver
UIO, VFIO, Bifurcated Driver
User Space
User Space
Kernel Space
Kernel Space
Multi-Cores
Multi-Cores
Lockless
Lockless
  • High Performance: Multi-Cores, Lockless
  • All-in-One: Reverse Proxy(FNAT|DR|Tunnel|NAT64), SNAT
  • Security: SYN-proxy, Allow/Deny ACL
  • IDC Friendly: VLAN, Bonding, IP Tunnel
  • QoS: Connection Limitation,  Traffic Control
High Performance: Multi-Cores, LocklessAll-in-One: Reverse Proxy(FNAT|DR|Tunnel|NAT64), SNATSecurity:...
Users
Users
Internet
Internet
Router
Router
DPVS Cluster
DPVS Cluster
Backend Servers
Backend Servers
\ No newline at end of file diff --git a/pic/modules.png b/pic/modules.png index 6a00fca3e415a983b25cd65ae16a01f38ad0ee43..46be449d4fed963d17fe2208b966e537082f542b 100644 GIT binary patch literal 252711 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 literal 50640 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 KDOEnQiugal302Sl diff --git a/pic/modules.svg b/pic/modules.svg new file mode 100644 index 000000000..809ab2220 --- /dev/null +++ b/pic/modules.svg @@ -0,0 +1,4 @@ + + + +
Traffic Control
Traffic Contr...
DPDK PMD Driver
DPDK PMD Driver
netif_addr
netif_addr
netif_flow
netif_flow
netif_port
netif_port
bonding
bonding
vlan
vlan
ip-tunnel
ip-tunnel
kni
kni
Job Scheduler
Job Scheduler
INET
INET
inetaddr
inetaddr
Routing
Routing
ICMPv6
ICMPv6
ICMP
ICMP
IPv4
IPv4
ndisc/ARP
ndisc/ARP
IPv6
IPv6
ipset
ipset
LLDP
LLDP
stats
stats
conn
conn
redirect
redirect
Protocol
TCP|UDP|SCTP
Protocol...
Scheduler
wrr|wlc|conhash|mh|fo
Scheduler...
serv%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22%26lt%3Bspan%20style%3D%26quot%3Bfont-size%3A%2016px%3B%26quot%3B%26gt%3Bconn%26lt%3B%2Fspan%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfillColor%3D%23dae8fc%3BstrokeColor%3D%236c8ebf%3BgradientColor%3D%237ea6e0%3BfontStyle%3D1%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%2250%22%20y%3D%22400%22%20width%3D%2280%22%20height%3D%2230%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3Eice
serv%3CmxGrap...
dest
dest
ProxyProto
ProxyProto
xmit
xmit
ipvsadm
ipvsadm
dpip
dpip
keepalived
keepalived
lcore-msg
(lockless)
lcore-msg...
sapool
sapool
timer
timer
kmod
(toa|uoa)
kmod...
config
config
dpvs.conf
dpvs.conf
keepalived
.conf
keepaliv...
healthcheck
healthcheck
dpvs-agent
dpvs-agent
Control Plane
Control Plane
API
API
Network
Devices
Network...
Lite Network Stack
Lite Ne...
Load Balancer
Load Ba...
Tools
Tools
ACL
ACL
\ No newline at end of file diff --git a/src/VERSION b/src/VERSION index be46f5131..2aba272e3 100755 --- a/src/VERSION +++ b/src/VERSION @@ -1,14 +1,36 @@ #!/bin/sh # program: dpvs -# Mar 12, 2024 # +# Sep 13, 2024 # ## +# Features +# - dpvs: Support QUIC/HTTP3, add nginx patches and facilitating code snippets for use of quic. +# - dpvs: Support SCTP forwarding implementation. +# - dpvs: Support LLDP protocol. +# - dpvs: Update default dpdk version to dpdk-stable-20.11.10. +# - dpvs: IPVS supports ipset based allow/deny list which allows for cidr acl rule. +# - dpvs: Support IPv6 link-local address auto configuration. +# - tools: Add ipset supports in dpvs-agent. +# - tools: Add snapshot caches for dpvs-agent virtual server apis. +# - doc: Update README.md. +# # Bugfixes -# - tools: Fix concurrency problem between dpvs-agent and healthcheck in editing realserver . -# - tools/dpvs-agent: Add the snapshot cache. -# - tools/healthchech: Fix occasionally arising bad icmp checksum problem for udp and udpping checkers. +# - dpvs: Fix multicast address sync problems and add dpip supports for multicast address lookup. +# - dpvs: Fix build errors and warnings with gcc verison 8.0+. +# - dpvs: Fix coredump problem when starting dpvs with insufficient memory. +# - dpvs: Use dpdk random generator in critical datapath for performance enhancement. +# - dpvs: Fix ipset default address family problem. +# - dpvs: Fix segmentation fault problem when running on machines whose cpu number is over DPVS_MAX_LCORE. +# - dpvs: Refactor netif_rte_port_alloc with netif_alloc. +# - dpvs: Fix prolems in IPv6 all-nodes and all-routers address initialization. +# - tools: Fix concurrency racing problem when dpvs-agent and healthcheck changing rs simultaneously. +# - tools: Fix healthchech bad icmp checksum problem ocasionally appeared in udp and udpping checkers. +# - tools: Fix keepalived quorum up script not excuted problem when old rs removed and new ones added in a reload. +# - tools: Fix ipvsadm local IP won't remove problem. +# - tools: Fix ipset list-all problem and improve efficiency. +# - tools: Fix dpip delay problem when list empty ipset with sorting enabled. # export VERSION=1.9 -export RELEASE=7 +export RELEASE=8 echo $VERSION-$RELEASE From 663466daa70ec74f6be3704c493481b0a0f55e47 Mon Sep 17 00:00:00 2001 From: ywc689 Date: Thu, 19 Sep 2024 17:33:26 +0800 Subject: [PATCH 62/63] scripts: fix param setting problem in dpdk-build.sh Signed-off-by: ywc689 --- scripts/dpdk-build.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/dpdk-build.sh b/scripts/dpdk-build.sh index 3e2d1a4b6..600a263d4 100755 --- a/scripts/dpdk-build.sh +++ b/scripts/dpdk-build.sh @@ -9,9 +9,8 @@ dpdkver=20.11.10 # default dpdk version (use stable v tarball=dpdk-${dpdkver}.tar.xz srcdir=dpdk-stable-$dpdkver -workdir=$(pwd)/dpdk # default work directory -patchdir=$(pwd)/patch/dpdk-stable-$dpdkver # default dpdk patch directory - +workdir="" +patchdir="" function help() { @@ -23,6 +22,13 @@ function help() echo -e "\033[31m -p specify the dpdk patch directory, default $(pwd)/patch/dpdk-stable-$dpdkver\033[0m" } +function set_dpdk_version() +{ + dpdkver=$1 + tarball=dpdk-${dpdkver}.tar.xz + srcdir=dpdk-stable-$dpdkver +} + function set_work_directory() { [ ! -d $1 ] && return 1 @@ -38,7 +44,7 @@ function set_patch_directory() ## parse args while getopts "hw:p:dv:" OPT; do case $OPT in - v) dpdkver=$OPTARG;; + v) set_dpdk_version $OPTARG;; w) set_work_directory $OPTARG ;; p) set_patch_directory $OPTARG;; d) build_options="${build_options} ${debug_options}";; @@ -46,6 +52,9 @@ while getopts "hw:p:dv:" OPT; do esac done +[ -z "$workdir" ] && workdir=$(pwd)/dpdk # use default work directory +[ -z "$patchdir" ] && patchdir=$(pwd)/patch/dpdk-stable-$dpdkver # use default dpdk patch directory + [ ! -d $workdir ] && mkdir $workdir echo -e "\033[32mwork directory: $workdir\033[0m" From 69101a3bd6bac62c323b67a0577688fc2202cdbc Mon Sep 17 00:00:00 2001 From: ywc689 Date: Thu, 19 Sep 2024 17:35:30 +0800 Subject: [PATCH 63/63] netif: fix memory corruption problem when retrieving nic's xstats Signed-off-by: ywc689 --- src/VERSION | 3 ++- src/netif.c | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/VERSION b/src/VERSION index 2aba272e3..f97d35bb7 100755 --- a/src/VERSION +++ b/src/VERSION @@ -1,6 +1,6 @@ #!/bin/sh # program: dpvs -# Sep 13, 2024 # +# Sep 19, 2024 # ## # Features # - dpvs: Support QUIC/HTTP3, add nginx patches and facilitating code snippets for use of quic. @@ -22,6 +22,7 @@ # - dpvs: Fix segmentation fault problem when running on machines whose cpu number is over DPVS_MAX_LCORE. # - dpvs: Refactor netif_rte_port_alloc with netif_alloc. # - dpvs: Fix prolems in IPv6 all-nodes and all-routers address initialization. +# - dpvs: Fix memory corruption problem when retrieving nic's xstats. # - tools: Fix concurrency racing problem when dpvs-agent and healthcheck changing rs simultaneously. # - tools: Fix healthchech bad icmp checksum problem ocasionally appeared in udp and udpping checkers. # - tools: Fix keepalived quorum up script not excuted problem when old rs removed and new ones added in a reload. diff --git a/src/netif.c b/src/netif.c index 5d8fe0b3d..0a071a1dc 100644 --- a/src/netif.c +++ b/src/netif.c @@ -3371,7 +3371,7 @@ static int netif_op_get_xstats(struct netif_port *dev, netif_nic_xstats_get_t ** if (nentries < 0) return EDPVS_DPDKAPIFAIL; - get = rte_calloc("xstats_get", 1, nentries * sizeof(struct netif_nic_xstats_entry), 0); + get = rte_calloc("xstats_get", 1, sizeof(*get) + nentries * sizeof(struct netif_nic_xstats_entry), 0); if (unlikely(!get)) return EDPVS_NOMEM; xstats = rte_calloc("xstats", 1, nentries * sizeof(struct rte_eth_xstat), 0);