From 790253c9b6366d8151d5340a9f89a88374a11f17 Mon Sep 17 00:00:00 2001 From: Mojtaba Date: Wed, 29 Nov 2023 16:14:56 +0100 Subject: [PATCH] feat: API server --- .gitignore | 1 + Makefile | 7 +- api/v1/api.go | 83 +++++++++++++++++ api/v1/bandwidth.go | 67 ++++++++++++++ api/v1/bandwidth_test.go | 96 +++++++++++++++++++ api/v1/errors.go | 48 ++++++++++ api/v1/index.go | 63 +++++++++++++ api/v1/index_test.go | 25 +++++ api/v1/latency.go | 71 +++++++++++++++ api/v1/latency_test.go | 99 ++++++++++++++++++++ api/v1/net_services.go | 72 +++++++++++++++ api/v1/net_services_utils.go | 172 +++++++++++++++++++++++++++++++++++ api/v1/packetloss.go | 68 ++++++++++++++ api/v1/packetloss_test.go | 96 +++++++++++++++++++ api/v1/suite_test.go | 87 ++++++++++++++++++ api/v1/types.go | 35 +++++++ api/v1/utils.go | 33 +++++++ api/v1/utils_test.go | 74 +++++++++++++++ cmd/cmd_utils.go | 6 +- cmd/serve.go | 52 +++++++++++ go.mod | 17 +++- go.sum | 33 ++++--- xdp/bandwidth/bandwidth.go | 3 + xdp/latency/latency.go | 3 + xdp/packetloss/packetloss.go | 6 +- 25 files changed, 1293 insertions(+), 24 deletions(-) create mode 100644 api/v1/api.go create mode 100644 api/v1/bandwidth.go create mode 100644 api/v1/bandwidth_test.go create mode 100644 api/v1/errors.go create mode 100644 api/v1/index.go create mode 100644 api/v1/index_test.go create mode 100644 api/v1/latency.go create mode 100644 api/v1/latency_test.go create mode 100644 api/v1/net_services.go create mode 100644 api/v1/net_services_utils.go create mode 100644 api/v1/packetloss.go create mode 100644 api/v1/packetloss_test.go create mode 100644 api/v1/suite_test.go create mode 100644 api/v1/types.go create mode 100644 api/v1/utils.go create mode 100644 api/v1/utils_test.go create mode 100644 cmd/serve.go diff --git a/.gitignore b/.gitignore index aca9544..dbe279a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ go.work bin/ *.o run.sh +.vscode/ diff --git a/Makefile b/Makefile index eecb3e4..2831789 100644 --- a/Makefile +++ b/Makefile @@ -11,9 +11,8 @@ build: docker: docker build -t bittwister . -# `run` is used to ease the developer life -run: all - sudo ./bin/$(BINARY_NAME) start -d wlp3s0 -b 500 +test-go: + sudo go test -v ./... -count=1 -p=1 test-packetloss: @bash ./scripts/tests/packetloss.sh @@ -27,6 +26,6 @@ test-latency: test-jitter: @bash ./scripts/tests/jitter.sh -test: test-packetloss test-bandwidth test-latency test-jitter +test: test-go test-packetloss test-bandwidth test-latency test-jitter .PHONY: all generate build run test \ No newline at end of file diff --git a/api/v1/api.go b/api/v1/api.go new file mode 100644 index 0000000..12c14d1 --- /dev/null +++ b/api/v1/api.go @@ -0,0 +1,83 @@ +package api + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "go.uber.org/zap" +) + +func path(endpoint string) string { + return fmt.Sprintf("/api/v1%s", endpoint) +} + +func NewRESTApiV1(productionMode bool, logger *zap.Logger) *RESTApiV1 { + restAPI := &RESTApiV1{ + router: mux.NewRouter(), + logger: logger, + loggerNoStack: logger.WithOptions(zap.AddStacktrace(zap.DPanicLevel)), + productionMode: productionMode, + } + + restAPI.router.HandleFunc("/", restAPI.IndexPage).Methods(http.MethodGet, http.MethodPost, http.MethodOptions, http.MethodPut, http.MethodHead) + + restAPI.router.HandleFunc(path("/packetloss/start"), restAPI.PacketlossStart).Methods(http.MethodPost) + restAPI.router.HandleFunc(path("/packetloss/status"), restAPI.PacketlossStatus).Methods(http.MethodGet) + restAPI.router.HandleFunc(path("/packetloss/stop"), restAPI.PacketlossStop).Methods(http.MethodPost) + + restAPI.router.HandleFunc(path("/bandwidth/start"), restAPI.BandwidthStart).Methods(http.MethodPost) + restAPI.router.HandleFunc(path("/bandwidth/status"), restAPI.BandwidthStatus).Methods(http.MethodGet) + restAPI.router.HandleFunc(path("/bandwidth/stop"), restAPI.BandwidthStop).Methods(http.MethodPost) + + restAPI.router.HandleFunc(path("/latency/start"), restAPI.LatencyStart).Methods(http.MethodPost) + restAPI.router.HandleFunc(path("/latency/status"), restAPI.LatencyStatus).Methods(http.MethodGet) + restAPI.router.HandleFunc(path("/latency/stop"), restAPI.LatencyStop).Methods(http.MethodPost) + + restAPI.router.HandleFunc(path("/services/status"), restAPI.NetServicesStatus).Methods(http.MethodGet) + + return restAPI +} + +func (a *RESTApiV1) Serve(addr, originAllowed string) error { + http.Handle("/", a.router) + + headersOk := handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Content-Length", "Accept-Encoding", "Authorization", "X-CSRF-Token"}) + originsOk := handlers.AllowedOrigins([]string{originAllowed}) + methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS"}) + + a.logger.Info(fmt.Sprintf("serving on %s", addr)) + + a.server = &http.Server{ + Addr: addr, + Handler: handlers.CORS(originsOk, headersOk, methodsOk)(a.router), + } + + return a.server.ListenAndServe() +} + +func (a *RESTApiV1) Shutdown() error { + if a.server == nil { + return errors.New("server is not running") + } + return a.server.Shutdown(context.Background()) +} + +func (a *RESTApiV1) GetAllAPIs() []string { + list := []string{} + err := a.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + apiPath, err := route.GetPathTemplate() + if err == nil { + list = append(list, apiPath) + } + return err + }) + if err != nil { + a.logger.Error("error while getting all APIs", zap.Error(err)) + } + + return list +} diff --git a/api/v1/bandwidth.go b/api/v1/bandwidth.go new file mode 100644 index 0000000..4f27102 --- /dev/null +++ b/api/v1/bandwidth.go @@ -0,0 +1,67 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/celestiaorg/bittwister/xdp/bandwidth" + "go.uber.org/zap" +) + +// BandwidthStart implements POST /bandwidth/start +func (a *RESTApiV1) BandwidthStart(resp http.ResponseWriter, req *http.Request) { + var body BandwidthStartRequest + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + sendJSONError(resp, + MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugJSONDecodeFailed, + Title: "JSON decode failed", + Message: err.Error(), + }, + http.StatusBadRequest) + return + } + + if a.bw == nil { + a.bw = &netRestrictService{ + service: &bandwidth.Bandwidth{ + Limit: body.Limit, + }, + logger: a.logger, + } + } else { + bw, ok := a.bw.service.(*bandwidth.Bandwidth) + if !ok { + sendJSONError(resp, + MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugTypeError, + Title: "Type cast error", + Message: "could not cast netRestrictService.service to *packetloss.PacketLoss", + }, + http.StatusInternalServerError) + return + } + bw.Limit = body.Limit + } + + err := netServiceStart(resp, a.bw, body.NetworkInterfaceName) + if err != nil { + a.loggerNoStack.Error("netServiceStart failed", zap.Error(err)) + } +} + +// BandwidthStop implements POST /bandwidth/stop +func (a *RESTApiV1) BandwidthStop(resp http.ResponseWriter, req *http.Request) { + if err := netServiceStop(resp, a.bw); err != nil { + a.loggerNoStack.Error("netServiceStop failed", zap.Error(err)) + } +} + +// BandwidthStatus implements GET /bandwidth/status +func (a *RESTApiV1) BandwidthStatus(resp http.ResponseWriter, _ *http.Request) { + if err := netServiceStatus(resp, a.bw); err != nil { + a.loggerNoStack.Error("netServiceStatus failed", zap.Error(err)) + } +} diff --git a/api/v1/bandwidth_test.go b/api/v1/bandwidth_test.go new file mode 100644 index 0000000..bb6cf96 --- /dev/null +++ b/api/v1/bandwidth_test.go @@ -0,0 +1,96 @@ +package api_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + + api "github.com/celestiaorg/bittwister/api/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func (s *APITestSuite) TestBandwidthStart() { + t := s.T() + + reqBody := api.BandwidthStartRequest{ + NetworkInterfaceName: s.ifaceName, + Limit: 100, + } + jsonBody, err := json.Marshal(reqBody) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, "/api/v1/bandwidth/start", bytes.NewReader(jsonBody)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + s.restAPI.BandwidthStart(rr, req) + // need to stop it to release the network interface for other tests + defer s.restAPI.BandwidthStop(rr, nil) + + assert.Equal(t, http.StatusOK, rr.Code) +} + +func (s *APITestSuite) TestBandwidthStop() { + t := s.T() + + reqBody := api.BandwidthStartRequest{ + NetworkInterfaceName: s.ifaceName, + Limit: 100, + } + jsonBody, err := json.Marshal(reqBody) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, "/api/v1/bandwidth/start", bytes.NewReader(jsonBody)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + s.restAPI.BandwidthStart(rr, req) + + require.NoError(t, waitForService(s.restAPI.BandwidthStatus)) + + rr = httptest.NewRecorder() + s.restAPI.BandwidthStop(rr, nil) + require.Equal(t, http.StatusOK, rr.Code) + + slug, err := getServiceStatusSlug(s.restAPI.BandwidthStatus) + require.NoError(t, err) + assert.Equal(t, api.SlugServiceNotReady, slug) +} + +func (s *APITestSuite) TestBandwidthStatus() { + t := s.T() + + slug, err := getServiceStatusSlug(s.restAPI.BandwidthStatus) + require.NoError(t, err) + if slug != api.SlugServiceNotReady && slug != api.SlugServiceNotInitialized { + t.Fatalf("unexpected service status: %s", slug) + } + + reqBody := api.BandwidthStartRequest{ + NetworkInterfaceName: s.ifaceName, + Limit: 100, + } + jsonBody, err := json.Marshal(reqBody) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, "/api/v1/bandwidth/start", bytes.NewReader(jsonBody)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + s.restAPI.BandwidthStart(rr, req) + + require.NoError(t, waitForService(s.restAPI.BandwidthStatus)) + + slug, err = getServiceStatusSlug(s.restAPI.BandwidthStatus) + require.NoError(t, err) + assert.Equal(t, api.SlugServiceReady, slug) + + s.restAPI.BandwidthStop(rr, nil) + require.Equal(t, http.StatusOK, rr.Code) + + slug, err = getServiceStatusSlug(s.restAPI.BandwidthStatus) + require.NoError(t, err) + assert.Equal(t, api.SlugServiceNotReady, slug) +} diff --git a/api/v1/errors.go b/api/v1/errors.go new file mode 100644 index 0000000..59fa90d --- /dev/null +++ b/api/v1/errors.go @@ -0,0 +1,48 @@ +package api + +import ( + "errors" +) + +const ( + APIMetaMessageTypeInfo = "info" + APIMetaMessageTypeWarning = "warning" + APIMetaMessageTypeError = "error" +) + +const ( + SlugServiceAlreadyStarted = "service-already-started" + SlugServiceStartFailed = "service-start-failed" + SlugServiceStopFailed = "service-stop-failed" + SlugServiceNotStarted = "service-not-started" + SlugServiceNotInitialized = "service-not-initialized" + SlugServiceReady = "service-ready" + SlugServiceNotReady = "service-not-ready" + SlugJSONDecodeFailed = "json-decode-failed" + SlugTypeError = "type-error" +) + +type MetaMessage struct { + Type string `json:"type"` // info, warning, error + Slug string `json:"slug"` + Title string `json:"title"` + Message string `json:"message"` +} + +var ( + ErrServiceNotInitialized = errors.New(SlugServiceNotInitialized) + ErrServiceAlreadyStarted = errors.New(SlugServiceAlreadyStarted) + ErrServiceNotStarted = errors.New(SlugServiceNotStarted) + ErrServiceStopFailed = errors.New(SlugServiceStopFailed) + ErrServiceStartFailed = errors.New(SlugServiceStartFailed) +) + +// convert a ApiMetaMessage to map[string]interface{} +func (m MetaMessage) ToMap() map[string]interface{} { + return map[string]interface{}{ + "type": m.Type, + "slug": m.Slug, + "title": m.Title, + "message": m.Message, + } +} diff --git a/api/v1/index.go b/api/v1/index.go new file mode 100644 index 0000000..1c73ad5 --- /dev/null +++ b/api/v1/index.go @@ -0,0 +1,63 @@ +package api + +import ( + "fmt" + "net/http" + "os" + "runtime/debug" + "strings" +) + +// IndexPage implements GET / +func (a *RESTApiV1) IndexPage(resp http.ResponseWriter, _ *http.Request) { + if a.productionMode { + return + } + + modName := "unknown" + buildInfo := "" + if bi, ok := debug.ReadBuildInfo(); ok { + modName = bi.Path + + buildInfo += "

Build Info:

" + for _, s := range bi.Settings { + buildInfo += fmt.Sprintf("", s.Key, s.Value) + } + buildInfo += "
%s%s
" + } + + html := `` + + html += fmt.Sprintf("Ciao, this is `%v` \n\n

", modName) + allAPIs := a.GetAllAPIs() + html += "

List of endpoints:

" + for _, a := range allAPIs { + + href := strings.TrimPrefix(a, "/") // it fixes the links if the service is running under a path + html += fmt.Sprintf(`%s
`, href, a) + } + + html += fmt.Sprintf("
Production Mode: %v", os.Getenv("PRODUCTION_MODE")) + html += buildInfo + + resp.Header().Set("Content-Type", "text/html; charset=utf-8") + _, err := resp.Write([]byte(html)) + if err != nil { + a.logger.Error(fmt.Sprintf("api `IndexPage`: %v", err)) + } +} diff --git a/api/v1/index_test.go b/api/v1/index_test.go new file mode 100644 index 0000000..986a8b9 --- /dev/null +++ b/api/v1/index_test.go @@ -0,0 +1,25 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestRESTApiV1_IndexPage(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + api := NewRESTApiV1(false, logger) + + req := httptest.NewRequest("GET", "/", nil) + rr := httptest.NewRecorder() + api.IndexPage(rr, req) + resp := rr.Result() + + assert.Equal(t, http.StatusOK, resp.StatusCode, "unexpected status code") +} diff --git a/api/v1/latency.go b/api/v1/latency.go new file mode 100644 index 0000000..75594dc --- /dev/null +++ b/api/v1/latency.go @@ -0,0 +1,71 @@ +package api + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/celestiaorg/bittwister/xdp/latency" + "go.uber.org/zap" +) + +// PacketlossStart implements POST /packetloss/start +func (a *RESTApiV1) LatencyStart(resp http.ResponseWriter, req *http.Request) { + var body LatencyStartRequest + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + sendJSONError(resp, + MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugJSONDecodeFailed, + Title: "JSON decode failed", + Message: err.Error(), + }, + http.StatusBadRequest) + return + } + + if a.lt == nil { + a.lt = &netRestrictService{ + service: &latency.Latency{ + Latency: time.Duration(body.Latency) * time.Millisecond, + Jitter: time.Duration(body.Jitter) * time.Millisecond, + }, + logger: a.logger, + } + } else { + lt, ok := a.lt.service.(*latency.Latency) + if !ok { + sendJSONError(resp, + MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugTypeError, + Title: "Type cast error", + Message: "could not cast netRestrictService.service to *packetloss.PacketLoss", + }, + http.StatusInternalServerError) + return + } + + lt.Latency = time.Duration(body.Latency) * time.Millisecond + lt.Jitter = time.Duration(body.Jitter) * time.Millisecond + } + + err := netServiceStart(resp, a.lt, body.NetworkInterfaceName) + if err != nil { + a.loggerNoStack.Error("netServiceStart failed", zap.Error(err)) + } +} + +// LatencyStop implements POST /latency/stop +func (a *RESTApiV1) LatencyStop(resp http.ResponseWriter, req *http.Request) { + if err := netServiceStop(resp, a.lt); err != nil { + a.loggerNoStack.Error("netServiceStop failed", zap.Error(err)) + } +} + +// LatencyStatus implements GET /latency/status +func (a *RESTApiV1) LatencyStatus(resp http.ResponseWriter, _ *http.Request) { + if err := netServiceStatus(resp, a.lt); err != nil { + a.loggerNoStack.Error("netServiceStatus failed", zap.Error(err)) + } +} diff --git a/api/v1/latency_test.go b/api/v1/latency_test.go new file mode 100644 index 0000000..ac1b4c9 --- /dev/null +++ b/api/v1/latency_test.go @@ -0,0 +1,99 @@ +package api_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + + api "github.com/celestiaorg/bittwister/api/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func (s *APITestSuite) TestLatencyStart() { + t := s.T() + + reqBody := api.LatencyStartRequest{ + NetworkInterfaceName: s.ifaceName, + Latency: 100, + Jitter: 50, + } + jsonBody, err := json.Marshal(reqBody) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, "/api/v1/latency/start", bytes.NewReader(jsonBody)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + s.restAPI.LatencyStart(rr, req) + // need to stop it to release the network interface for other tests + defer s.restAPI.LatencyStop(rr, nil) + + assert.Equal(t, http.StatusOK, rr.Code) +} + +func (s *APITestSuite) TestLatencyStop() { + t := s.T() + + reqBody := api.LatencyStartRequest{ + NetworkInterfaceName: s.ifaceName, + Latency: 100, + Jitter: 50, + } + jsonBody, err := json.Marshal(reqBody) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, "/api/v1/latency/start", bytes.NewReader(jsonBody)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + s.restAPI.LatencyStart(rr, req) + + require.NoError(t, waitForService(s.restAPI.LatencyStatus)) + + rr = httptest.NewRecorder() + s.restAPI.LatencyStop(rr, nil) + require.Equal(t, http.StatusOK, rr.Code) + + slug, err := getServiceStatusSlug(s.restAPI.LatencyStatus) + require.NoError(t, err) + assert.Equal(t, api.SlugServiceNotReady, slug) +} + +func (s *APITestSuite) TestLatencyStatus() { + t := s.T() + + slug, err := getServiceStatusSlug(s.restAPI.LatencyStatus) + require.NoError(t, err) + if slug != api.SlugServiceNotReady && slug != api.SlugServiceNotInitialized { + t.Fatalf("unexpected service status: %s", slug) + } + + reqBody := api.LatencyStartRequest{ + NetworkInterfaceName: s.ifaceName, + Latency: 100, + Jitter: 50, + } + jsonBody, err := json.Marshal(reqBody) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, "/api/v1/latency/start", bytes.NewReader(jsonBody)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + s.restAPI.LatencyStart(rr, req) + + require.NoError(t, waitForService(s.restAPI.LatencyStatus)) + + slug, err = getServiceStatusSlug(s.restAPI.LatencyStatus) + require.NoError(t, err) + assert.Equal(t, api.SlugServiceReady, slug) + + s.restAPI.LatencyStop(rr, nil) + require.Equal(t, http.StatusOK, rr.Code) + + slug, err = getServiceStatusSlug(s.restAPI.LatencyStatus) + require.NoError(t, err) + assert.Equal(t, api.SlugServiceNotReady, slug) +} diff --git a/api/v1/net_services.go b/api/v1/net_services.go new file mode 100644 index 0000000..a475b6a --- /dev/null +++ b/api/v1/net_services.go @@ -0,0 +1,72 @@ +package api + +import ( + "net/http" + + "github.com/celestiaorg/bittwister/xdp/bandwidth" + "github.com/celestiaorg/bittwister/xdp/latency" + "github.com/celestiaorg/bittwister/xdp/packetloss" + "go.uber.org/zap" +) + +type ServiceStatus struct { + Name string `json:"name"` + Ready bool `json:"ready"` + NetworkInterfaceName string `json:"network_interface_name"` + Params map[string]interface{} `json:"params"` // key:value +} + +// NetServicesStatus implements GET /services/status +func (a *RESTApiV1) NetServicesStatus(resp http.ResponseWriter, req *http.Request) { + out := make([]ServiceStatus, 0, 3) + for _, ns := range []*netRestrictService{a.pl, a.bw, a.lt} { + if ns == nil { + continue + } + + var ( + params = make(map[string]interface{}) + name string + netIfaceName string + ) + + if s, ok := ns.service.(*packetloss.PacketLoss); ok { + name = "packetloss" + params["packet_loss_rate"] = s.PacketLossRate + netIfaceName = s.NetworkInterface.Name + + } else if s, ok := ns.service.(*bandwidth.Bandwidth); ok { + name = "bandwidth" + params["limit"] = s.Limit + netIfaceName = s.NetworkInterface.Name + + } else if s, ok := ns.service.(*latency.Latency); ok { + name = "latency" + params["latency_ms"] = s.Latency.Milliseconds() + params["jitter_ms"] = s.Jitter.Milliseconds() + netIfaceName = s.NetworkInterface.Name + + } else { + sendJSONError(resp, + MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugTypeError, + Title: "Type cast error", + Message: "could not cast netRestrictService.service to *packetloss.PacketLoss, *bandwidth.Bandwidth or *latency.Latency", + }, + http.StatusInternalServerError) + return + } + + out = append(out, ServiceStatus{ + Name: name, + Ready: ns.service.Ready(), + NetworkInterfaceName: netIfaceName, + Params: params, + }) + } + + if err := sendJSON(resp, out); err != nil { + a.loggerNoStack.Error("sendJSON failed", zap.Error(err)) + } +} diff --git a/api/v1/net_services_utils.go b/api/v1/net_services_utils.go new file mode 100644 index 0000000..2301b04 --- /dev/null +++ b/api/v1/net_services_utils.go @@ -0,0 +1,172 @@ +package api + +import ( + "context" + "fmt" + "net" + "net/http" + "time" + + "github.com/celestiaorg/bittwister/xdp" + "github.com/celestiaorg/bittwister/xdp/bandwidth" + "github.com/celestiaorg/bittwister/xdp/latency" + "github.com/celestiaorg/bittwister/xdp/packetloss" + "go.uber.org/zap" +) + +const ServiceStopTimeout = 5 // Seconds + +type netRestrictService struct { + service xdp.XdpLoader + ctx context.Context + cancel context.CancelFunc + logger *zap.Logger +} + +func (n *netRestrictService) Start(networkInterfaceName string) error { + if n.service == nil { + return ErrServiceNotInitialized + } + + iface, err := net.InterfaceByName(networkInterfaceName) + if err != nil { + return fmt.Errorf("lookup network device %q: %v", networkInterfaceName, err) + } + + if s, ok := n.service.(*packetloss.PacketLoss); ok { + s.NetworkInterface = iface + } else if s, ok := n.service.(*bandwidth.Bandwidth); ok { + s.NetworkInterface = iface + } else if s, ok := n.service.(*latency.Latency); ok { + s.NetworkInterface = iface + } else { + return fmt.Errorf("could not cast netRestrictService.service to *packetloss.PacketLoss, *bandwidth.Bandwidth or *latency.Latency") + } + + n.ctx, n.cancel = context.WithCancel(context.Background()) + go n.service.Start(n.ctx, n.logger) + + return nil +} + +func (n *netRestrictService) Stop() error { + if n.cancel == nil { + return ErrServiceNotStarted + } + + n.cancel() + + if n.service.Ready() { + return ErrServiceStopFailed + } + return nil +} + +func netServiceStart(resp http.ResponseWriter, ns *netRestrictService, ifaceName string) error { + if ns == nil || ns.service == nil { + sendJSONError(resp, MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugServiceNotInitialized, + Title: "Service not initiated", + Message: "To get the status of the service, it must be started first.", + }, http.StatusOK) + return ErrServiceNotInitialized + } + + if ns.service.Ready() { + sendJSONError(resp, MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugServiceAlreadyStarted, + Title: "Service already started", + Message: "To start the service again, it must be stopped first.", + }, http.StatusBadRequest) + return ErrServiceAlreadyStarted + } + + if err := ns.Start(ifaceName); err != nil { + sendJSONError(resp, + MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugServiceStartFailed, + Title: "Service start failed", + Message: err.Error(), + }, + http.StatusInternalServerError) + return err + } + return nil +} + +func netServiceStop(resp http.ResponseWriter, ns *netRestrictService) error { + if ns == nil || ns.service == nil { + sendJSONError(resp, MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugServiceNotInitialized, + Title: "Service not initiated", + Message: "To get the status of the service, it must be started first.", + }, http.StatusOK) + return ErrServiceNotInitialized + } + + ns.cancel() + + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + timeout := ServiceStopTimeout * 1000 / 100 + for range ticker.C { + timeout-- + if !ns.service.Ready() || timeout <= 0 { + break + } + } + + if ns.service.Ready() { + sendJSONError(resp, MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugServiceStopFailed, + Title: "Service stop failed", + Message: "The service could not be stopped.", + }, http.StatusInternalServerError) + return ErrServiceStopFailed + } + + err := sendJSON(resp, MetaMessage{ + Type: APIMetaMessageTypeInfo, + Slug: SlugServiceNotReady, + Title: "Service stopped", + }) + + if err != nil { + return fmt.Errorf("sendJSON failed: %w", err) + } + + return nil +} + +func netServiceStatus(resp http.ResponseWriter, ns *netRestrictService) error { + if ns == nil || ns.service == nil { + sendJSONError(resp, MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugServiceNotInitialized, + Title: "Service not initiated", + Message: "To get the status of the service, it must be started first.", + }, http.StatusOK) + return ErrServiceNotInitialized + } + + statusSlug := SlugServiceNotReady + if ns.service.Ready() { + statusSlug = SlugServiceReady + } + + err := sendJSON(resp, MetaMessage{ + Type: APIMetaMessageTypeInfo, + Slug: statusSlug, + Title: "Service status", + }) + + if err != nil { + return fmt.Errorf("sendJSON failed: %w", err) + } + return nil +} diff --git a/api/v1/packetloss.go b/api/v1/packetloss.go new file mode 100644 index 0000000..4e46684 --- /dev/null +++ b/api/v1/packetloss.go @@ -0,0 +1,68 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/celestiaorg/bittwister/xdp/packetloss" + "go.uber.org/zap" +) + +// PacketlossStart implements POST /packetloss/start +func (a *RESTApiV1) PacketlossStart(resp http.ResponseWriter, req *http.Request) { + var body PacketLossStartRequest + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + sendJSONError(resp, + MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugJSONDecodeFailed, + Title: "JSON decode failed", + Message: err.Error(), + }, + http.StatusBadRequest) + return + } + + if a.pl == nil { + a.pl = &netRestrictService{ + service: &packetloss.PacketLoss{ + NetworkInterface: nil, + PacketLossRate: body.PacketLossRate, + }, + logger: a.logger, + } + } else { + pl, ok := a.pl.service.(*packetloss.PacketLoss) + if !ok { + sendJSONError(resp, + MetaMessage{ + Type: APIMetaMessageTypeError, + Slug: SlugTypeError, + Title: "Type cast error", + Message: "could not cast netRestrictService.service to *packetloss.PacketLoss", + }, + http.StatusInternalServerError) + return + } + pl.PacketLossRate = body.PacketLossRate + } + + err := netServiceStart(resp, a.pl, body.NetworkInterfaceName) + if err != nil { + a.loggerNoStack.Error("netServiceStart failed", zap.Error(err)) + } +} + +// PacketlossStop implements POST /packetloss/stop +func (a *RESTApiV1) PacketlossStop(resp http.ResponseWriter, req *http.Request) { + if err := netServiceStop(resp, a.pl); err != nil { + a.loggerNoStack.Error("netServiceStop failed", zap.Error(err)) + } +} + +// PacketlossStatus implements GET /packetloss/status +func (a *RESTApiV1) PacketlossStatus(resp http.ResponseWriter, _ *http.Request) { + if err := netServiceStatus(resp, a.pl); err != nil { + a.loggerNoStack.Error("netServiceStatus failed", zap.Error(err)) + } +} diff --git a/api/v1/packetloss_test.go b/api/v1/packetloss_test.go new file mode 100644 index 0000000..96dd655 --- /dev/null +++ b/api/v1/packetloss_test.go @@ -0,0 +1,96 @@ +package api_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + + api "github.com/celestiaorg/bittwister/api/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func (s *APITestSuite) TestPacketlossStart() { + t := s.T() + + reqBody := api.PacketLossStartRequest{ + NetworkInterfaceName: s.ifaceName, + PacketLossRate: 10, + } + jsonBody, err := json.Marshal(reqBody) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, "/api/v1/packetloss/start", bytes.NewReader(jsonBody)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + s.restAPI.PacketlossStart(rr, req) + // need to stop it to release the network interface for other tests + defer s.restAPI.PacketlossStop(rr, nil) + + assert.Equal(t, http.StatusOK, rr.Code) +} + +func (s *APITestSuite) TestPacketlossStop() { + t := s.T() + + reqBody := api.PacketLossStartRequest{ + NetworkInterfaceName: s.ifaceName, + PacketLossRate: 10, + } + jsonBody, err := json.Marshal(reqBody) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, "/api/v1/packetloss/start", bytes.NewReader(jsonBody)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + s.restAPI.PacketlossStart(rr, req) + + require.NoError(t, waitForService(s.restAPI.PacketlossStatus)) + + rr = httptest.NewRecorder() + s.restAPI.PacketlossStop(rr, nil) + require.Equal(t, http.StatusOK, rr.Code) + + slug, err := getServiceStatusSlug(s.restAPI.PacketlossStatus) + require.NoError(t, err) + assert.Equal(t, api.SlugServiceNotReady, slug) +} + +func (s *APITestSuite) TestPacketlossStatus() { + t := s.T() + + slug, err := getServiceStatusSlug(s.restAPI.PacketlossStatus) + require.NoError(t, err) + if slug != api.SlugServiceNotReady && slug != api.SlugServiceNotInitialized { + t.Fatalf("unexpected service status: %s", slug) + } + + reqBody := api.PacketLossStartRequest{ + NetworkInterfaceName: s.ifaceName, + PacketLossRate: 10, + } + jsonBody, err := json.Marshal(reqBody) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, "/api/v1/packetloss/start", bytes.NewReader(jsonBody)) + require.NoError(t, err) + + rr := httptest.NewRecorder() + s.restAPI.PacketlossStart(rr, req) + + require.NoError(t, waitForService(s.restAPI.PacketlossStatus)) + + slug, err = getServiceStatusSlug(s.restAPI.PacketlossStatus) + require.NoError(t, err) + assert.Equal(t, api.SlugServiceReady, slug) + + s.restAPI.PacketlossStop(rr, nil) + require.Equal(t, http.StatusOK, rr.Code) + + slug, err = getServiceStatusSlug(s.restAPI.PacketlossStatus) + require.NoError(t, err) + assert.Equal(t, api.SlugServiceNotReady, slug) +} diff --git a/api/v1/suite_test.go b/api/v1/suite_test.go new file mode 100644 index 0000000..fa3a0e1 --- /dev/null +++ b/api/v1/suite_test.go @@ -0,0 +1,87 @@ +package api_test + +import ( + "encoding/json" + "errors" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + api "github.com/celestiaorg/bittwister/api/v1" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/zap" +) + +type APITestSuite struct { + suite.Suite + + logger *zap.Logger + restAPI *api.RESTApiV1 + ifaceName string +} + +func (s *APITestSuite) SetupSuite() { + t := s.T() + logger, err := zap.NewDevelopment() + require.NoError(t, err) + s.logger = logger + + s.restAPI = api.NewRESTApiV1(false, s.logger) + + ifaceName, err := getLoopbackInterfaceName() + require.NoError(t, err) + s.ifaceName = ifaceName +} + +func TestAPI(t *testing.T) { + suite.Run(t, new(APITestSuite)) +} + +func getServiceStatusSlug(statusFunc func(http.ResponseWriter, *http.Request)) (string, error) { + rr := httptest.NewRecorder() + statusFunc(rr, nil) + if rr.Code != http.StatusOK { + return "", errors.New("failed to get service status") + } + + var msg api.MetaMessage + err := json.NewDecoder(rr.Body).Decode(&msg) + if err != nil { + return "", err + } + + return msg.Slug, nil +} + +// Wait for the service to be started +func waitForService(statusFunc func(http.ResponseWriter, *http.Request)) error { + for i := 0; i < 10; i++ { + slug, err := getServiceStatusSlug(statusFunc) + if err != nil { + return err + } + if slug == api.SlugServiceReady { + return nil + } + time.Sleep(500 * time.Millisecond) + } + return errors.New("timeout waiting for service to start") +} + +func getLoopbackInterfaceName() (string, error) { + interfaces, err := net.Interfaces() + if err != nil { + return "", err + } + + for _, iface := range interfaces { + if iface.Flags&net.FlagLoopback != 0 { + return iface.Name, nil + } + } + + return "", errors.New("loopback interface not found") +} diff --git a/api/v1/types.go b/api/v1/types.go new file mode 100644 index 0000000..8b001bf --- /dev/null +++ b/api/v1/types.go @@ -0,0 +1,35 @@ +package api + +import ( + "net/http" + + "github.com/gorilla/mux" + "go.uber.org/zap" +) + +type RESTApiV1 struct { + router *mux.Router + server *http.Server + logger *zap.Logger + loggerNoStack *zap.Logger + + pl, bw, lt *netRestrictService + + productionMode bool +} + +type PacketLossStartRequest struct { + NetworkInterfaceName string `json:"network_interface"` + PacketLossRate int32 `json:"packet_loss_rate"` +} + +type BandwidthStartRequest struct { + NetworkInterfaceName string `json:"network_interface"` + Limit int64 `json:"limit"` +} + +type LatencyStartRequest struct { + NetworkInterfaceName string `json:"network_interface"` + Latency int64 `json:"latency_ms"` + Jitter int64 `json:"jitter_ms"` +} diff --git a/api/v1/utils.go b/api/v1/utils.go new file mode 100644 index 0000000..26bbc22 --- /dev/null +++ b/api/v1/utils.go @@ -0,0 +1,33 @@ +package api + +import ( + "encoding/json" + "net/http" +) + +func sendJSON(resp http.ResponseWriter, obj interface{}) error { + data, err := json.MarshalIndent(obj, "", " ") + if err != nil { + http.Error(resp, "Internal Server Error: "+err.Error(), http.StatusInternalServerError) + return err + } + + resp.Header().Set("Content-Type", "application/json") + _, err = resp.Write(data) + if err != nil { + http.Error(resp, "Internal Server Error: "+err.Error(), http.StatusInternalServerError) + return err + } + + return nil +} + +func sendJSONError(resp http.ResponseWriter, obj interface{}, code int) { + data, err := json.MarshalIndent(obj, "", " ") + if err != nil { + http.Error(resp, "Internal Server Error: "+err.Error(), http.StatusInternalServerError) + return + } + + http.Error(resp, string(data), code) +} diff --git a/api/v1/utils_test.go b/api/v1/utils_test.go new file mode 100644 index 0000000..f25c8cd --- /dev/null +++ b/api/v1/utils_test.go @@ -0,0 +1,74 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSendJson(t *testing.T) { + testCases := []struct { + name string + resp *httptest.ResponseRecorder + obj interface{} + expected string + hasError bool + }{ + { + name: "valid input", + resp: httptest.NewRecorder(), + obj: map[string]string{ + "name": "Gholi Sibil", + "age": "30", + }, + expected: "{\n \"age\": \"30\",\n \"name\": \"Gholi Sibil\"\n}", + hasError: false, + }, + { + name: "invalid input", + resp: httptest.NewRecorder(), + obj: make(chan int), + expected: "", + hasError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := sendJSON(tc.resp, tc.obj) + if tc.hasError { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.JSONEq(t, tc.expected, tc.resp.Body.String(), "response body should match the expected JSON") + assert.Equal(t, "application/json", tc.resp.Header().Get("Content-Type")) + }) + } +} + +func TestSendJsonError(t *testing.T) { + resp := httptest.NewRecorder() + obj := MetaMessage{ + Type: APIMetaMessageTypeInfo, + Slug: "test", + Title: "Test Message", + Message: "This is a test error message", + } + code := http.StatusBadRequest + + sendJSONError(resp, obj, code) + + assert.Equal(t, code, resp.Code, "response code should be equal to the passed code") + + expectedBody := `{ + "type": "info", + "slug": "test", + "title": "Test Message", + "message": "This is a test error message" + }` + assert.JSONEq(t, expectedBody, resp.Body.String(), "response body should match the expected JSON") +} diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 5c8a8ff..5b2c536 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -42,7 +42,11 @@ func getLogger(logLevel string, productionMode bool) (*zap.Logger, error) { } var err error - cfg.Level, err = zap.ParseAtomicLevel(logLevel) + level := zap.NewAtomicLevel() + if err := level.UnmarshalText([]byte(logLevel)); err != nil { + return nil, fmt.Errorf("getLogger unmarshal level %q: %v", logLevel, err) + } + cfg.Level = level if err != nil { return nil, fmt.Errorf("getLogger: %v", err) } diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..64d5018 --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "fmt" + + api "github.com/celestiaorg/bittwister/api/v1" + "github.com/spf13/cobra" +) + +const ( + flagServeAddr = "serve-addr" +) + +var flagsServe struct { + serveAddr string + originAllowed string + logLevel string + productionMode bool +} + +func init() { + rootCmd.AddCommand(serveCmd) + + serveCmd.PersistentFlags().StringVar(&flagsServe.serveAddr, flagServeAddr, ":9007", "address to serve on") + serveCmd.PersistentFlags().StringVar(&flagsServe.originAllowed, "origin-allowed", "*", "origin allowed for CORS") + + serveCmd.PersistentFlags().StringVar(&flagsServe.logLevel, flagLogLevel, "info", "log level (e.g. debug, info, warn, error, dpanic, panic, fatal)") + serveCmd.PersistentFlags().BoolVar(&flagsServe.productionMode, flagProductionMode, false, "production mode (e.g. disable debug logs)") +} + +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "serves the Bit Twister API server", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + logger, err := getLogger(flagsServe.logLevel, flagsServe.productionMode) + if err != nil { + return err + } + defer func() { + // The error is ignored because of this issue: https://github.com/uber-go/zap/issues/328 + _ = logger.Sync() + }() + + logger.Info("Starting the API server...") + + restAPI := api.NewRESTApiV1(flagsServe.productionMode, logger) + logger.Fatal(fmt.Sprintf("REST API server: %v", restAPI.Serve(flagsServe.serveAddr, flagsServe.originAllowed))) + + return nil + }, +} diff --git a/go.mod b/go.mod index a97b6d2..24f2a8f 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,23 @@ go 1.21.0 require ( github.com/cilium/ebpf v0.12.3 + github.com/gorilla/handlers v1.5.2 + github.com/gorilla/mux v1.8.1 github.com/spf13/cobra v1.8.0 - go.uber.org/zap v1.26.0 + github.com/stretchr/testify v1.8.4 + go.uber.org/zap v1.11.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - go.uber.org/multierr v1.10.0 // indirect - golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect - golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/sys v0.15.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b59eb29..4897c02 100644 --- a/go.sum +++ b/go.sum @@ -3,16 +3,24 @@ github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1 github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -22,18 +30,19 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= -golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c h1:3kC/TjQ+xzIblQv39bCOyRk8fbEeJcDHwbyxPUU2BpA= -golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.11.0 h1:gSmpCfs+R47a4yQPAI4xJ0IPDLTRGXskm6UelqNXpqE= +go.uber.org/zap v1.11.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/xdp/bandwidth/bandwidth.go b/xdp/bandwidth/bandwidth.go index 0d99ed6..05778ee 100644 --- a/xdp/bandwidth/bandwidth.go +++ b/xdp/bandwidth/bandwidth.go @@ -56,6 +56,9 @@ func (b *Bandwidth) Start(ctx context.Context, logger *zap.Logger) { b.ready = true <-ctx.Done() + + b.ready = false + logger.Info(fmt.Sprintf("Bandwidth limiter stopped on device %q", b.NetworkInterface.Name)) } func (b *Bandwidth) Ready() bool { diff --git a/xdp/latency/latency.go b/xdp/latency/latency.go index 3fd411b..4497aa3 100644 --- a/xdp/latency/latency.go +++ b/xdp/latency/latency.go @@ -56,6 +56,9 @@ func (l *Latency) Start(ctx context.Context, logger *zap.Logger) { if err := l.deleteTc(); err != nil { logger.Fatal("failed to delete tc rule", zap.Error(err)) } + + l.ready = false + logger.Info(fmt.Sprintf("Latency/Jitter stopped on device %q", l.NetworkInterface.Name)) } func (l *Latency) Ready() bool { diff --git a/xdp/packetloss/packetloss.go b/xdp/packetloss/packetloss.go index 78dd14e..ee44c83 100644 --- a/xdp/packetloss/packetloss.go +++ b/xdp/packetloss/packetloss.go @@ -48,17 +48,17 @@ func (p *PacketLoss) Start(ctx context.Context, logger *zap.Logger) { } logger.Info( - fmt.Sprintf("Packet loss started with rate %d%% on device %q", + fmt.Sprintf("Packetloss started with rate %d%% on device %q", p.PacketLossRate, p.NetworkInterface.Name, ), ) p.ready = true - <-ctx.Done() - fmt.Printf("Packet loss stopped.") + p.ready = false + logger.Info(fmt.Sprintf("Packetloss stopped on device %q", p.NetworkInterface.Name)) } func (p *PacketLoss) Ready() bool {