diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 544f286..e808bc4 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -38,4 +38,4 @@ jobs: cache: true - name: Run linter - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v5 diff --git a/.reuse/dep5 b/.reuse/dep5 index fa896b7..2f9a6ed 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -3,10 +3,6 @@ Upstream-Name: Gont Upstream-Contact: Steffen Vogel Source: https://github.com/cunicu/gont -Files: docs/images/* docs/CNAME README.md +Files: flake.lock docs/images/* docs/CNAME README.md go.sum .renovaterc.json Copyright: 2023 Steffen Vogel License: Apache-2.0 - -Files: go.sum .renovaterc.json -Copyright: 2023 Steffen Vogel -License: Apache-2.0 \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 240345b..136ce33 100644 --- a/docs/index.html +++ b/docs/index.html @@ -9,10 +9,10 @@ Gont - A testing framework for distributed Go applications - - - - + + + + @@ -28,10 +28,10 @@ - - - - + + + + diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5e9fe1f --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1716137900, + "narHash": "sha256-sowPU+tLQv8GlqtVtsXioTKeaQvlMz/pefcdwg8MvfM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6c0b7a92c30122196a761b440ac0d46d3d9954f1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..ace243d --- /dev/null +++ b/flake.nix @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2023 Steffen Vogel +# SPDX-License-Identifier: Apache-2.0 +{ + description = "Gont: A Go testing framework for distributed applications"; + + inputs = { + flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + }; + + outputs = + { + self, + flake-utils, + nixpkgs, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + rec { + packages.gont = pkgs.buildGoModule { + name = "gont"; + src = ./.; + vendorHash = "sha256-QOh1jBR7FL/fKFmJv7wGxuCghRLR3DV/0TzXd+bUFP0="; + buildInputs = with pkgs; [ libpcap ]; + doCheck = false; + }; + + devShell = pkgs.mkShell { + packages = with pkgs; [ + golangci-lint + reuse + traceroute + gnumake + tshark + packages.gont + ]; + + inputsFrom = [ packages.gont ]; + }; + + formatter = nixpkgs.nixfmt-rfc-style; + } + ); +} diff --git a/go.mod b/go.mod index d918938..d3632e8 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,9 @@ require ( github.com/vishvananda/netlink v1.2.1-beta.2.0.20221214185949-378a404a26f0 github.com/vishvananda/netns v0.0.4 go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 - golang.org/x/net v0.22.0 - golang.org/x/sys v0.18.0 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/net v0.25.0 + golang.org/x/sys v0.20.0 kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 ) @@ -38,7 +38,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/sync v0.6.0 // indirect + golang.org/x/sync v0.7.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect diff --git a/go.sum b/go.sum index 91adeba..038a591 100644 --- a/go.sum +++ b/go.sum @@ -308,6 +308,16 @@ golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4 golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc= golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -351,6 +361,10 @@ golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -365,6 +379,8 @@ golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -404,6 +420,10 @@ golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/utils/utils.go b/internal/utils/utils.go index a1cc35a..c9fc702 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -6,9 +6,14 @@ package utils import ( "math/rand" "os" + "path/filepath" ) func Touch(path string) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL, 0o444) if err != nil { return err diff --git a/pkg/base_node.go b/pkg/base_node.go index 93f923a..3e63cb6 100644 --- a/pkg/base_node.go +++ b/pkg/base_node.go @@ -73,7 +73,7 @@ func (n *Network) AddNode(name string, opts ...Option) (*BaseNode, error) { } } - // Create mount point dirs + // Create mount point directories for _, ed := range node.EmptyDirs { path := filepath.Join(basePath, "files", ed) diff --git a/pkg/cmd_test.go b/pkg/cmd_test.go index 5d64321..18aa5d1 100644 --- a/pkg/cmd_test.go +++ b/pkg/cmd_test.go @@ -13,6 +13,7 @@ import ( g "cunicu.li/gont/v2/pkg" co "cunicu.li/gont/v2/pkg/options/cmd" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -152,3 +153,18 @@ func TestCmdContext(t *testing.T) { require.True(t, ws.Signaled()) require.Equal(t, syscall.SIGKILL, ws.Signal()) } + +func TestIProute2Files(t *testing.T) { + n, err := g.NewNetwork("") + require.NoError(t, err, "Failed to create network") + defer n.Close() + + beep, err := n.AddHost("beep") + require.Nil(t, err) + + cmd := beep.Command("ip", "addr") + out, err := cmd.CombinedOutput() + assert.Nil(t, err) + + t.Logf("Output: %s", out) +} diff --git a/pkg/exec.go b/pkg/exec.go index 3dfdb6e..7d8efa1 100644 --- a/pkg/exec.go +++ b/pkg/exec.go @@ -4,6 +4,7 @@ package gont import ( + "errors" "fmt" "os" "path/filepath" @@ -120,6 +121,16 @@ func setupBindMounts(basePath string) error { src := filepath.Join(filesRootPath, path) tgt := filepath.Join("/", path) + _, err := os.Stat(src) + if err != nil { + return fmt.Errorf("failed to stat source: %s: %w", src, err) + } + + // If target doesn't exist, there's nothing to mount + if _, err := os.Stat(tgt); errors.Is(err, os.ErrNotExist) { //nolint:nestif + continue + } + if err := syscall.Mount(src, tgt, "", syscall.MS_BIND, ""); err != nil { return fmt.Errorf("failed to mount: %w", err) } diff --git a/pkg/nat_http_test.go b/pkg/nat_http_test.go index faf39d6..177ffba 100644 --- a/pkg/nat_http_test.go +++ b/pkg/nat_http_test.go @@ -62,7 +62,7 @@ func TestGetMyIP(t *testing.T) { require.NoError(t, err, "Failed to setup default route") outp := &bytes.Buffer{} - _, err = client.Run("curl", "-sk", "--connect-timeout", 1000, "https://server", + _, err = client.Run("curl", "--silent", "--insecure", "--connect-timeout", 1000, "https://server", co.Stdout(outp)) require.NoError(t, err, "Request failed") diff --git a/pkg/network_files.go b/pkg/network_files.go index f80b6ce..d76c32c 100644 --- a/pkg/network_files.go +++ b/pkg/network_files.go @@ -4,12 +4,16 @@ package gont import ( + "bufio" "fmt" "io" "net" "os" "path/filepath" + "slices" "strings" + + "cunicu.li/gont/v2/internal/utils" ) // IPv4loopback is the IPv4 loopback address (127.0.0.1) @@ -90,10 +94,105 @@ func (n *Network) WriteHostsFile(f io.Writer) error { } func (n *Network) GenerateConfigFiles() error { - return n.GenerateIProute2Files() + if err := n.generateIProute2Files(); err != nil { + return err + } + + // We need to patch /etc/nsswitch.conf here + // to avoid using systemd-resolved here as its + // broken from within network namesapces + if err := n.patchNSSConfFile(); err != nil { + return err + } + + // We also need to hide the NSCD socket as glibc + // will otherwise use NSCD and hence again the resolve + // NSS module provided by systemd-resolved. + if err := n.hideNSCDSocket(); err != nil { + return err + } + + return nil +} + +func (n *Network) hideNSCDSocket() error { + fn := filepath.Join(n.VarPath, "files/var/run/nscd/socket") + + // We hide the NSCD socket here by bind mounting + // an empty file over its location. + return utils.Touch(fn) +} + +func readNSSwitchConfig(fn string) (map[string][]string, error) { + f, err := os.Open(fn) + if err != nil { + return nil, err + } + + m := map[string][]string{} + s := bufio.NewScanner(f) + + for s.Scan() { + line := s.Text() + + if strings.HasPrefix(line, "#") { + continue // Skip comments + } + + cols := strings.Split(line, ":") + + db := cols[0] + srcs := cols[1:] + + m[db] = srcs + } + + if err := s.Err(); err != nil { + return nil, err + } + + return nil, nil +} + +func writeNSSwitchConfig(fn string, config map[string][]string) error { + f, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer f.Close() + + if _, err = fmt.Fprintln(f, "# Gont's patched nsswitch.conf"); err != nil { + return err + } + + for db, srcs := range config { + if _, err := fmt.Fprintf(f, "%s: %s\n", db, strings.Join(srcs, " ")); err != nil { + return err + } + } + + return err +} + +func (n *Network) patchNSSConfFile() error { + cfg, err := readNSSwitchConfig("/etc/nsswitch.conf") + if err != nil { + return fmt.Errorf("failed to read nsswitch.conf: %w", err) + } + + for db := range cfg { + if db == "hosts" { + cfg[db] = slices.DeleteFunc(cfg[db], func(src string) bool { + return !strings.HasPrefix(src, "resolve") && !strings.HasPrefix(src, "mymachines") && !strings.HasPrefix(src, "myhostname") + }) + } + } + + fn := filepath.Join(n.VarPath, "files/etc/nsswitch.conf") + return writeNSSwitchConfig(fn, cfg) } -func (n *Network) GenerateIProute2Files() error { +func (n *Network) generateIProute2Files() error { fn := filepath.Join(n.VarPath, "files/etc/iproute2/group") if err := os.MkdirAll(filepath.Dir(fn), 0o755); err != nil { return err diff --git a/pkg/options/cmd/exec.go b/pkg/options/cmd/exec.go index b0faa6a..35fa68a 100644 --- a/pkg/options/cmd/exec.go +++ b/pkg/options/cmd/exec.go @@ -75,11 +75,17 @@ func (e Env) ApplyExecCmd(c *exec.Cmd) { c.Env = append(c.Env, string(e)) } -// EnvVar appends a key-value paired environment variable +// EnvVar appends a key-value paired environment variable. func EnvVar(k, v string) Env { return Env(fmt.Sprintf("%s=%s", k, v)) } +// PassEnv forward environment variables from the main +// Gont process to the invoked sub-processes. +func PassEnv(k string) Env { + return EnvVar(k, os.Getenv(k)) +} + // Envs appends additional environment variables. type Envs []string