Skip to content

Add a new integration test suite checking network behaviors#45850

Closed
akerouanton wants to merge 9 commits intomoby:masterfrom
akerouanton:networking-test-suite
Closed

Add a new integration test suite checking network behaviors#45850
akerouanton wants to merge 9 commits intomoby:masterfrom
akerouanton:networking-test-suite

Conversation

@akerouanton
Copy link
Member

@akerouanton akerouanton commented Jun 29, 2023

- What I did

This PR introduces a new integration test suite aimed at testing networking features like inter-container communication, network isolation, port mapping, what happens when the userland proxy is enabled or disabled, etc... and how these features interact with daemon-level and network-level parameters.

So far, there's pretty much no tests making sure our networks are working as expected. For instance, there's a few tests for port mapping, but they don't cover all cases (eg. like IPv6, disabling the userland proxy, etc...); 2. there're a few tests that check if a specific iptables rule exists, but that doesn't prevent that specific rule from be wrong in the first place.

As we're planning to refactor how iptables rules are written and change some of them to fix known security issues, we need a way to test all parameter combinations (at least, the important ones). So far, this was done by hand which is a particularly painful and time consuming experience. As such, this new test suite is foundational to upcoming work.

This test suite should help:

  1. Assert networking security boundaries are working properly, and prevent regressions on security patches ;
  2. Discover what's missing to make the userland proxy disabled by default ;
  3. Give a canonical test plan for new port mappers ;

Following tests are implemented in this PR:

  • Inter-container communication for internal and non-internal bridge networks, over IPv4 and IPv6 (using ULAs or link-local addresses, SLAAC-assigned or dynamically allocated) ;
  • Inter-network communication for internal and non-internal bridge networks, over IPv4 and IPv6, is disallowed ;
  • Connecting to published ports from the host, using either IPv4 or IPv6 loopback address or another local address ;
  • Connecting to published ports from another bridge, using the bridge gateway address with either the userland proxy enabled or disabled ;
  • Connecting to ports bound to loopback address, from another host running on a shared L2 segment ;
  • Reaching a container that didn't publish any port, from another host running on a shared L2 segment ;
  • Connecting to a published port bound to a specific IP address, from another host running on another L2 segment ;

I plan to write some more tests in the near future (eg. for CVE-2020-13401).

- How to verify it

CI is green.

- A picture of a cute animal (not mandatory but encouraged)

@akerouanton akerouanton force-pushed the networking-test-suite branch from 9dbbec0 to a477f71 Compare June 29, 2023 20:32
Copy link
Contributor

@corhere corhere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see the libnetwork testutils package moved under libnetwork/internal/ so we don't have to worry about some third party importing it.

@akerouanton
Copy link
Member Author

I would like to see the libnetwork testutils package moved under libnetwork/internal/ so we don't have to worry about some third party importing it.

We can't move testutils into libnetwork/internal and use it from github.com/docker/docker/integration as internal packages can only be used by other packages in the parent directory they're rooted at. From https://dave.cheney.net/2019/10/06/use-internal-packages-to-reduce-your-public-api-surface:

For example, a package /a/b/c/internal/d/e/f can only be imported by code in the directory tree rooted at /a/b/c. It cannot be imported by code in /a/b/g or in any other repository.

@akerouanton akerouanton force-pushed the networking-test-suite branch 5 times, most recently from 59b4203 to 0fc8c2d Compare July 24, 2023 15:08
@akerouanton akerouanton force-pushed the networking-test-suite branch 10 times, most recently from 8bdf860 to b26b1ce Compare July 24, 2023 19:27
@corhere
Copy link
Contributor

corhere commented Jul 24, 2023

We can't move testutils into libnetwork/internal and use it from github.com/docker/docker/integration

Fine, github.com/docker/docker/internal/... then. I don't care how, I just want it so that external modules can't import the package so we can make breaking changes to it at any time.

@akerouanton akerouanton force-pushed the networking-test-suite branch 10 times, most recently from c20dadf to b28358c Compare July 26, 2023 14:51
This commit introduces a new integration test suite aimed at testing
networking features like inter-container communication, network
isolation, port mapping, etc... and how they interact with daemon-level
and network-level parameters.

So far, there's pretty much no tests making sure our networks are well
configured: 1. there're a few tests for port mapping, but they don't
cover all use cases ; 2. there're a few tests that check if a specific
iptables rule exist, but that doesn't prevent that specific iptables
rule to be wrong in the first place.

As we're planning to refactor how iptables rules are written, and change
some of them to fix known security issues, we need a way to test all
combinations of parameters. So far, this was done by hand, which is
particularly painful and time consuming. As such, this new test suite is
foundational to upcoming work.

Following tests are implemented in this specific commit:

- Inter-container communications for internal and non-internal
  bridge networks, over IPv4 and IPv6.
- Inter-container communications using IPv6 link-local addresses for
  internal and non-internal bridge networks.
- Inter-network communications for internal and non-internal bridge
  networks, over IPv4 and IPv6, are disallowed.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
Signed-off-by: Albin Kerouanton <albinker@gmail.com>
This commit adds a test to the new networking test suite that makes sure
we can connect to ports published on local host, from loopback address
or another local address.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
Signed-off-by: Albin Kerouanton <albinker@gmail.com>
Signed-off-by: Albin Kerouanton <albinker@gmail.com>
Signed-off-by: Albin Kerouanton <albinker@gmail.com>
@akerouanton akerouanton force-pushed the networking-test-suite branch 2 times, most recently from 903a916 to 593ec82 Compare July 27, 2023 00:54
@akerouanton akerouanton marked this pull request as ready for review July 27, 2023 08:32
@akerouanton akerouanton requested a review from tianon as a code owner July 27, 2023 08:32
@akerouanton akerouanton removed the request for review from tianon July 27, 2023 08:40
These tests use a SYN prober to check whether mapped ports are reachable
when they should not.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
Signed-off-by: Albin Kerouanton <albinker@gmail.com>
The tests introduced in "integration: Test port mapping security"
check victim hosts don't send a SYN-ACK when it should not to. This
could happen for two reasons: 1. because the right security measures
are effectively dropping the SYN packet ; or 2. because the SYN packet
was never received by the victim host.

The latter case could be a potential source of false positives, which
are pretty bad since we're testing security features. To alleviate this
risk, this commit adds the ability to capture packets on the victim's
host interface and leverage that to make sure the SYN packet is
well received. If it's not, the test fails.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
@akerouanton akerouanton force-pushed the networking-test-suite branch from 593ec82 to 397b812 Compare July 27, 2023 14:52
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some comments after making a first pass / glance over the changes 😅

Comment on lines +88 to +93
type RunResult struct {
ContainerID string
ExitCode int
Stdout *bytes.Buffer
Stderr *bytes.Buffer
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting to think if we could use icmd.Result for these, but it looks like it would need some contributing to upstream because the outBuffer and errBuffer are not exported.

Could be nice though to use that type for this as well 🤔

// Result stores the result of running a command
type Result struct {
Cmd *exec.Cmd
ExitCode int
Error error
// Timeout is true if the command was killed because it ran for too long
Timeout bool
outBuffer *lockedBuffer
errBuffer *lockedBuffer
}

I think some of this code also shows that we need to improve some of the client to not having to write utilities like this (e.g. the demultiplexing). Perhaps something to work on (separately) to move code from the CLI, and/or provide better utilities in the client code 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree on all counts.

Starting to think if we could use icmd.Result for these

While there are a lot of similarities, this is not the result of an *exec.Cmd run. The gotest.tools module is Apache2 licensed so there's nothing preventing us from copying that code and modifying it to fit the needs of asserting on the result of attaching to a container or running an exec.

I think some of this code also shows that we need to improve some of the client to not having to write utilities like this (e.g. the demultiplexing).

Demultiplex into what? Buffering the whole demuxed output into an in-memory buffer is rarely the right thing to do in general as the output of an arbitrary command may be unbounded. We shouldn't provide utilities in the client code which make it easier to do the wrong thing than to do the right thing. By only providing stdcopy the consumer of the client is forced to choose which io.Writer to demux the streams into, giving them the opportunity to consider which choice would best fit their use case.

Comment on lines +43 to +45
if err != nil && !os.IsNotExist(err) {
assert.NilError(t, err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this assert actually do anything? It would always be a non-nil error? Should this be a t.Fatal(err)?

Comment on lines +46 to +51
if err == nil {
if reusePrevious {
return newNs
}
t.Fatalf("Network namespace %s already exists whereas TEST_REUSE_L2SEGMENT is empty.", newNs.name)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is a bit confusing ... no error, and producing a t.Fatal() 🤔

Comment on lines +88 to +91
if err := netns.Set(orig.handle); err != nil {
t.Fatalf("could not switch back to the original netns: %v", err)
}
runtime.UnlockOSThread()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok to skip the runtime.UnlockOSThread() if we fail? (I guess it is if we os.Exit()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not only is it okay, it's mandatory. See #45850 (comment)

Comment on lines +151 to +156
if err := syscall.Unmount(ns.Path(), 0); err != nil {
logger.Logf("failed to unmount netns %s: %v", ns, err)
}
if err := os.Remove(ns.Path()); err != nil {
logger.Logf("failed to remove netns mountpoint %s: %v", ns, err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just double checking; we should remove the path even if we failed to unmount?

}

daddr := &packet.Addr{HardwareAddr: p.DstMAC}
fmt.Printf("Sending Ethernet frame to %s (%d bytes).\n%s\n", daddr.String(), len(buf.Bytes()), hex.Dump(buf.Bytes()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we need a context passed so allow us to past a test-logger (instead of fmt.Printf 🤔

Copy link
Contributor

@corhere corhere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please break this up into smaller PRs; this is way too much to review at once.

Comment on lines +137 to +139
if t.Failed() {
t.Logf("Logs from %s:\n%s", ctr2Name, logs)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test runner is perfectly capable of suppressing log output on passing tests. All you're doing by making this log conditional is breaking the -test.v flag. Don't break the -test.v flag.

Comment on lines +88 to +91
if err := netns.Set(orig.handle); err != nil {
t.Fatalf("could not switch back to the original netns: %v", err)
}
runtime.UnlockOSThread()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not only is it okay, it's mandatory. See #45850 (comment)

Comment on lines +88 to +93
type RunResult struct {
ContainerID string
ExitCode int
Stdout *bytes.Buffer
Stderr *bytes.Buffer
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree on all counts.

Starting to think if we could use icmd.Result for these

While there are a lot of similarities, this is not the result of an *exec.Cmd run. The gotest.tools module is Apache2 licensed so there's nothing preventing us from copying that code and modifying it to fit the needs of asserting on the result of attaching to a container or running an exec.

I think some of this code also shows that we need to improve some of the client to not having to write utilities like this (e.g. the demultiplexing).

Demultiplex into what? Buffering the whole demuxed output into an in-memory buffer is rarely the right thing to do in general as the output of an arbitrary command may be unbounded. We shouldn't provide utilities in the client code which make it easier to do the wrong thing than to do the right thing. By only providing stdcopy the consumer of the client is forced to choose which io.Writer to demux the streams into, giving them the opportunity to consider which choice would best fit their use case.

@akerouanton
Copy link
Member Author

A similar L3Segment was introduced in #48545. It uses iproute2 commands instead of a full Go implementation. This makes it easier to manually reproduce the environment where a specific test is failing. Since the introduction of this L3Segment, we've merged various integration tests that simulates networking between multiple hosts on the same L2 / L3 semgnet.

Most / all the test cases in this PR should be covered by newer tests, so let me close it.

@akerouanton akerouanton deleted the networking-test-suite branch November 22, 2024 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants