diff --git a/.github/workflows/tests-and-linters.yml b/.github/workflows/tests-and-linters.yml
index 49aa7a5131..602f816597 100644
--- a/.github/workflows/tests-and-linters.yml
+++ b/.github/workflows/tests-and-linters.yml
@@ -60,6 +60,9 @@ jobs:
- name: E2E basic test
run: go run mage.go -v TestE2EBasic
+ - name: E2E shaper test
+ run: go run mage.go -v TestE2EShaper
+
e2e-nat:
runs-on: ubuntu-latest
diff --git a/ci/test/e2e.go b/ci/test/e2e.go
index e14754b674..fd5b6973fa 100644
--- a/ci/test/e2e.go
+++ b/ci/test/e2e.go
@@ -24,6 +24,7 @@ import (
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
"github.com/mysteriumnetwork/node/e2e"
+ e2e_shaper "github.com/mysteriumnetwork/node/e2e/shaper"
"github.com/mysteriumnetwork/node/logconfig"
"github.com/rs/zerolog/log"
)
@@ -50,6 +51,23 @@ func BuildE2eTestBinary() error {
return os.Rename("./e2e.test", "./build/e2e/test")
}
+// BuildE2eShaperTestBinary builds the e2e test binary.
+func BuildE2eShaperTestBinary() error {
+ err := sh.RunWith(crossCompileFlags, "go", "test", "-c", "./e2e/shaper")
+ if err != nil {
+ return err
+ }
+ err = sh.RunWith(crossCompileFlags, "go", "build", "-o", "shaper.websvc", "./e2e/shaper/websvc")
+ if err != nil {
+ return err
+ }
+
+ _ = os.Mkdir("./build/e2e/", os.ModeDir)
+ os.Rename("./shaper.test", "./build/e2e/shaper.test")
+ os.Rename("./shaper.websvc", "./build/e2e/shaper.websvc")
+ return nil
+}
+
// BuildE2eDeployerBinary builds the deployer binary for e2e tests.
func BuildE2eDeployerBinary() error {
return sh.RunWith(crossCompileFlags, "go", "build", "-o", "./build/e2e/deployer", "./e2e/blockchain/deployer.go")
@@ -75,6 +93,24 @@ func TestE2EBasic() error {
return runner.Test("myst-provider")
}
+// TestE2EShaper runs end-to-end tests
+func TestE2EShaper() error {
+ logconfig.Bootstrap()
+
+ mg.Deps(BuildE2eShaperTestBinary)
+
+ composeFiles := []string{
+ "./docker-compose.e2e-shaper.yml",
+ }
+
+ runner, cleanup := e2e_shaper.NewRunner(composeFiles, "node_e2e_shaper_test", "")
+ defer cleanup()
+ if err := runner.Init(); err != nil {
+ return err
+ }
+ return runner.Test()
+}
+
// TestE2ENAT runs end-to-end tests in NAT environment
func TestE2ENAT() error {
logconfig.Bootstrap()
diff --git a/docker-compose.e2e-shaper.yml b/docker-compose.e2e-shaper.yml
new file mode 100644
index 0000000000..a049a571b7
--- /dev/null
+++ b/docker-compose.e2e-shaper.yml
@@ -0,0 +1,25 @@
+version: '3.0'
+services:
+
+ shaper-websvc:
+ build:
+ context: .
+ dockerfile: ./e2e/shaper/websvc/Dockerfile
+ cap_add:
+ - NET_ADMIN
+ working_dir: /node
+ expose:
+ - 8083
+
+ #go runner to run go programs inside localnet (usefull for contract deployment or e2e test running)
+ go-runner:
+ depends_on:
+ - shaper-websvc
+ build:
+ context: .
+ dockerfile: ./e2e/gorunner-shaper/Dockerfile.precompiled
+ cap_add:
+ - NET_ADMIN
+ volumes:
+ - ./e2e/blockchain/keystore:/node/keystore
+ working_dir: /node
diff --git a/e2e/gorunner-shaper/Dockerfile b/e2e/gorunner-shaper/Dockerfile
new file mode 100644
index 0000000000..acc0ca2727
--- /dev/null
+++ b/e2e/gorunner-shaper/Dockerfile
@@ -0,0 +1,3 @@
+FROM golang:1.20-alpine
+
+RUN apk add --no-cache bash gcc musl-dev make linux-headers iptables ipset ca-certificates openvpn bash sudo openresolv
diff --git a/e2e/gorunner-shaper/Dockerfile.precompiled b/e2e/gorunner-shaper/Dockerfile.precompiled
new file mode 100644
index 0000000000..3eed44d7e7
--- /dev/null
+++ b/e2e/gorunner-shaper/Dockerfile.precompiled
@@ -0,0 +1,6 @@
+FROM alpine:3.12
+
+RUN apk add --no-cache bash gcc musl-dev make linux-headers iptables ipset ca-certificates openvpn bash sudo openresolv
+RUN ln -s /sbin/iptables /usr/sbin/iptables
+
+COPY ./build/e2e/shaper.test /usr/local/bin/shaper.test
diff --git a/e2e/shaper/runner.go b/e2e/shaper/runner.go
new file mode 100644
index 0000000000..24b3494807
--- /dev/null
+++ b/e2e/shaper/runner.go
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The "MysteriumNetwork/node" Authors.
+ *
+ * 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 3 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package shaper
+
+import (
+ "github.com/magefile/mage/sh"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog/log"
+)
+
+// NewRunner returns e2e test runners instance
+func NewRunner(composeFiles []string, testEnv, services string) (runner *Runner, cleanup func()) {
+ fileArgs := make([]string, 0)
+ for _, f := range composeFiles {
+ fileArgs = append(fileArgs, "-f", f)
+ }
+ var args []string
+ args = append(args, fileArgs...)
+ args = append(args, "-p", testEnv)
+
+ runner = &Runner{
+ compose: sh.RunCmd("docker", append([]string{"compose"}, args...)...),
+ composeOut: sh.OutCmd("docker", append([]string{"compose"}, args...)...),
+ testEnv: testEnv,
+ services: services,
+ }
+ return runner, runner.cleanup
+}
+
+// Runner is e2e tests runner responsible for starting test environment and running e2e tests.
+type Runner struct {
+ compose func(args ...string) error
+ composeOut func(args ...string) (string, error)
+ etherPassphrase string
+ testEnv string
+ services string
+}
+
+// Test starts given provider and consumer nodes and runs e2e tests.
+func (r *Runner) Test() (retErr error) {
+ log.Info().Msg("Running tests for env: " + r.testEnv)
+
+ err := r.compose("run", "go-runner",
+ "/usr/local/bin/shaper.test",
+ )
+
+ retErr = errors.Wrap(err, "tests failed!")
+ return
+}
+
+func (r *Runner) cleanup() {
+ log.Info().Msg("Cleaning up")
+
+ _ = r.compose("logs")
+ if err := r.compose("down", "--volumes", "--remove-orphans", "--timeout", "30"); err != nil {
+ log.Warn().Err(err).Msg("Cleanup error")
+ }
+}
+
+// Init starts bug6022.test dependency
+func (r *Runner) Init() error {
+ log.Info().Msg("Starting other services")
+ if err := r.compose("pull"); err != nil {
+ return errors.Wrap(err, "could not pull images")
+ }
+
+ if err := r.compose("up", "-d", "shaper-websvc"); err != nil {
+ return errors.Wrap(err, "starting other services failed!")
+ }
+
+ log.Info().Msg("Building app images")
+ if err := r.compose("build"); err != nil {
+ return errors.Wrap(err, "building app images failed!")
+ }
+
+ return nil
+}
diff --git a/e2e/shaper/shaper_service_test.go b/e2e/shaper/shaper_service_test.go
new file mode 100644
index 0000000000..2d4ec49fc3
--- /dev/null
+++ b/e2e/shaper/shaper_service_test.go
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 The "MysteriumNetwork/node" Authors.
+ *
+ * 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 3 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package shaper
+
+import (
+ "bytes"
+ "encoding/hex"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "net/netip"
+ "net/url"
+ "strings"
+ "testing"
+
+ "golang.zx2c4.com/wireguard/conn"
+ "golang.zx2c4.com/wireguard/device"
+ "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+
+ "github.com/mysteriumnetwork/node/config"
+ netstack "github.com/mysteriumnetwork/node/services/wireguard/endpoint/netstack"
+ netstack_provider "github.com/mysteriumnetwork/node/services/wireguard/endpoint/netstack-provider"
+)
+
+func startClient(t *testing.T, priv, pubServ wgtypes.Key) {
+ tun, tnet, err := netstack.CreateNetTUN(
+ []netip.Addr{netip.MustParseAddr("192.168.4.100")},
+ []netip.Addr{netip.MustParseAddr("8.8.8.8")},
+ device.DefaultMTU)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelError, "C> "))
+
+ wgConf := &bytes.Buffer{}
+ wgConf.WriteString("private_key=" + hex.EncodeToString(priv[:]) + "\n")
+ wgConf.WriteString("public_key=" + hex.EncodeToString(pubServ[:]) + "\n")
+ wgConf.WriteString("allowed_ip=0.0.0.0/0\n")
+ wgConf.WriteString("endpoint=127.0.0.1:58120\n")
+
+ if err = dev.IpcSetOperation(wgConf); err != nil {
+ t.Error(err)
+ return
+ }
+ if err = dev.Up(); err != nil {
+ t.Error(err)
+ return
+ }
+
+ client := http.Client{
+ Transport: &http.Transport{
+ DialContext: tnet.DialContext,
+ },
+ }
+
+ // resolve docker container hostname
+ u, _ := url.Parse("http://shaper-websvc:8083/test")
+ address, err := net.LookupHost(u.Hostname())
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ u.Host = address[0] + ":" + u.Port()
+
+ resp, err := client.Get(u.String())
+ if err != nil {
+ t.Error(err)
+ log.Println(err)
+ return
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Error(err)
+ log.Println(err)
+ return
+ }
+ log.Println("Reply:", string(body))
+
+ res := strings.HasPrefix(string(body), "Hello,")
+ ok := "success"
+ if !res {
+ ok = "failed"
+ }
+ log.Println("Test result:", ok)
+}
+
+func startServer(t *testing.T, privKey, pubClinet wgtypes.Key) {
+ tun, _, _, err := netstack_provider.CreateNetTUNWithStack(
+ []netip.Addr{netip.MustParseAddr("192.168.4.1")},
+ 53,
+ device.DefaultMTU,
+ )
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelError, "S> "))
+
+ wgConf := &bytes.Buffer{}
+ wgConf.WriteString("private_key=" + hex.EncodeToString(privKey[:]) + "\n")
+ wgConf.WriteString("listen_port=58120\n")
+ wgConf.WriteString("public_key=" + hex.EncodeToString(pubClinet[:]) + "\n")
+ wgConf.WriteString("allowed_ip=0.0.0.0/0\n")
+
+ if err = dev.IpcSetOperation(wgConf); err != nil {
+ t.Error(err)
+ return
+ }
+ if err = dev.Up(); err != nil {
+ t.Error(err)
+ return
+ }
+}
+
+func TestShaperEnabled(t *testing.T) {
+ log.Default().SetFlags(0)
+
+ config.Current.SetDefault(config.FlagShaperBandwidth.Name, "6250")
+ config.Current.SetDefault(config.FlagShaperEnabled.Name, "true")
+ config.FlagFirewallProtectedNetworks.Value = "10.0.0.0/8,127.0.0.0/8" // 192.168.0.0/16,
+
+ netstack_provider.InitUserspaceShaper(nil)
+
+ privKey1, err := wgtypes.GeneratePrivateKey()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ privKey2, err := wgtypes.GeneratePrivateKey()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ _, _ = privKey1, privKey2
+
+ startServer(t, privKey1, privKey2.PublicKey())
+ startClient(t, privKey2, privKey1.PublicKey())
+}
diff --git a/e2e/shaper/websvc/Dockerfile b/e2e/shaper/websvc/Dockerfile
new file mode 100644
index 0000000000..15126cac1a
--- /dev/null
+++ b/e2e/shaper/websvc/Dockerfile
@@ -0,0 +1,5 @@
+FROM alpine:3.12
+
+COPY ./build/e2e/shaper.websvc /usr/local/bin/shaper.websvc
+
+ENTRYPOINT ["/usr/local/bin/shaper.websvc"]
diff --git a/e2e/shaper/websvc/main.go b/e2e/shaper/websvc/main.go
new file mode 100644
index 0000000000..ae49399d0c
--- /dev/null
+++ b/e2e/shaper/websvc/main.go
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The "MysteriumNetwork/node" Authors.
+ *
+ * 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 3 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package main
+
+import (
+ "fmt"
+ "net/http"
+)
+
+func main() {
+ http.HandleFunc("/", handler)
+ http.ListenAndServe(":8083", nil)
+}
+
+func handler(w http.ResponseWriter, r *http.Request) {
+ fmt.Println(r.URL.Path, r.RemoteAddr)
+ fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
+}
diff --git a/services/wireguard/endpoint/netstack-provider/shaper_service.go b/services/wireguard/endpoint/netstack-provider/shaper_service.go
index e32fa9edf3..b0d7c940df 100644
--- a/services/wireguard/endpoint/netstack-provider/shaper_service.go
+++ b/services/wireguard/endpoint/netstack-provider/shaper_service.go
@@ -18,13 +18,18 @@
package netstack_provider
import (
+ "time"
+
"github.com/mysteriumnetwork/node/config"
"github.com/mysteriumnetwork/node/eventbus"
"github.com/rs/zerolog/log"
"golang.org/x/time/rate"
)
-const AppTopicConfigShaper = "config:shaper"
+const (
+ AppTopicConfigShaper = "config:shaper"
+ BurstLimit = 1000 * 1000 * 1000
+)
var rateLimiter *rate.Limiter
@@ -33,7 +38,7 @@ func getRateLimitter() *rate.Limiter {
}
func InitUserspaceShaper(eventBus eventbus.EventBus) {
- applyLimits := func(e interface{}) {
+ applyLimits := func(_ interface{}) {
bandwidthBytes := config.GetUInt64(config.FlagShaperBandwidth) * 1024
bandwidth := rate.Limit(bandwidthBytes)
if !config.GetBool(config.FlagShaperEnabled) {
@@ -43,9 +48,14 @@ func InitUserspaceShaper(eventBus eventbus.EventBus) {
rateLimiter.SetLimit(bandwidth)
}
- rateLimiter = rate.NewLimiter(rate.Inf, 0)
+ rateLimiter = rate.NewLimiter(rate.Inf, BurstLimit)
+ rateLimiter.AllowN(time.Now(), BurstLimit) // spend initial burst
+
applyLimits(nil)
+ if eventBus == nil {
+ return
+ }
err := eventBus.SubscribeAsync(AppTopicConfigShaper, applyLimits)
if err != nil {
log.Error().Msgf("could not subscribe to topic: %v", err)