Files
Ricardo MaraschiniandGitHub e272683bce feat: implement collector and analyser for network namespace connectivity (#1670)
* feat: implement collector and analyser for network namespace connectivity

checks if two network namespaces can talk to each other on udp and tcp.
its usage is as follows:

```yaml
apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
  name: test
spec:
  hostCollectors:
  - networkNamespaceConnectivity:
      collectorName: check-network-connectivity
      fromCIDR: 10.0.0.0/24
      toCIDR: 10.0.1.0/24
  hostAnalyzers:
  - networkNamespaceConnectivity:
      collectorName: check-network-connectivity
      outcomes:
      - pass:
          message: "Communication between 10.0.0.0/24 and 10.0.1.0/24 is working"
      - fail:
          message: "Communication between 10.0.0.0/24 and 10.0.1.0/24 isn't working"
```

if this fails then you may need to enable `forwarding` with:

```bash
sysctl -w net.ipv4.ip_forward=1
```

if it still fails then you may need to configure firewalld to allow the
traffic or simply disable it for sake of testing.

* chore: rebuild schemas

* chore: remove unused property

* chore: disable namespaces for other platforms

* chore: make sure we timeout temporary servers

* feat: analyzer now supports multi-node collection

* feat: check both udp and tcp even on failure

check both protocols even if one fails. this pr commit also introduces a
timeout that can be set by the user.

* feat: add templating to the failure outcome

allow users to dump the errors found during the analysis.

* chore: addressing pr comments

* feat: delete interface pair before namespace

even though the interface pair is deleted everyttime we delete the
namespace on my tests we better delete it before we delete the
namespace.

this comes out of a review comment where some people seem to still be
able to see the interface pair even after the namespace is deleted.

i.e. better safe than sorry.

* chore: fix typo on comment
2024-11-06 11:30:13 +01:00

218 lines
6.5 KiB
Go

//go:build linux
package namespaces
import (
"fmt"
"net"
"time"
)
type NamespacePinger struct {
*ManagedNetworkNamespace
cfg Configuration
}
// PingUDP communicates with the provided IP address from within the namespace.
// This functions sends an UDP packet and expects to receive an echo back.
func (n *NamespacePinger) PingUDP(dst net.IP) error {
n.cfg.Logf("reaching to %q from %q with udp", dst, n.InternalIP)
pinger := func() error {
addr := &net.UDPAddr{IP: dst, Port: n.cfg.Port}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
return fmt.Errorf("error dialing udp: %w", err)
}
defer conn.Close()
if _, err = conn.Write([]byte("echo")); err != nil {
return fmt.Errorf("error writing to udp socket: %w", err)
}
deadline := time.Now().Add(n.cfg.Timeout)
if err := conn.SetReadDeadline(deadline); err != nil {
return fmt.Errorf("error setting udp read deadline: %w", err)
}
// XXX: review this buffer size and validation.
buffer := make([]byte, 6)
if _, _, err = conn.ReadFromUDP(buffer); err != nil {
return fmt.Errorf("error reading from udp socket: %w", err)
}
return nil
}
return n.Run(pinger)
}
// PingTCP communicates with the provided IP address from within the namespace.
// This functions sends an TCP packet and expects to receive an echo back.
func (n *NamespacePinger) PingTCP(dst net.IP) error {
n.cfg.Logf("reaching to %q from %q with tcp", dst, n.InternalIP)
pinger := func() error {
addr := fmt.Sprintf("%s:%d", dst, n.cfg.Port)
conn, err := net.DialTimeout("tcp", addr, n.cfg.Timeout)
if err != nil {
return fmt.Errorf("error dialing tcp: %w", err)
}
defer conn.Close()
if _, err = conn.Write([]byte("echo")); err != nil {
return fmt.Errorf("error writing to tcp socket: %w", err)
}
// XXX: review this buffer size and validation.
buffer := make([]byte, 6)
if _, err = conn.Read(buffer); err != nil {
return fmt.Errorf("error reading from tcp socket: %w", err)
}
return nil
}
return n.Run(pinger)
}
// StartTCPEchoServer is a helper to run startTCPEchoServer inside a goroutine.
// This function blocks until the server is ready to receive packets or failed
// to start. Errors are sent to the provided channel.
func (n *NamespacePinger) StartTCPEchoServer(errors chan error) {
ready := make(chan struct{})
go func() {
errors <- n.startTCPEchoServer(ready)
}()
<-ready
}
// startTCPEchoServer starts a tcp server inside the namespace. The thread
// running the goroutine that process this call will be moved to the namespace.
// This echo servers just returns "echo" as a response. Once one packet is
// received, the server ends. Callers must wait until the ready channel is
// closed before they can start sending packets.
func (n *NamespacePinger) startTCPEchoServer(ready chan struct{}) (err error) {
addr := fmt.Sprintf("%s:%d", n.InternalIP, n.cfg.Port)
n.cfg.Logf("starting tcp echo server on namespace %q(%q)", n.name, addr)
if err = n.Join(); err != nil {
close(ready)
return fmt.Errorf("error joining namespace: %w", err)
}
defer func() {
err = WrapIfFail("error leaving namespace", err, n.Leave)
}()
listener, err := net.Listen("tcp", addr)
if err != nil {
close(ready)
return fmt.Errorf("error starting tcp server: %w", err)
}
defer listener.Close()
deadline := time.Now().Add(n.cfg.Timeout)
tcplistener := listener.(*net.TCPListener)
if err = tcplistener.SetDeadline(deadline); err != nil {
close(ready)
return fmt.Errorf("error setting tcp listener deadline: %w", err)
}
go func() {
// XXX: here be dragons. we can't signalize we are ready until
// the call to read is done so we artificially sleep for a bit
// here.
time.Sleep(100 * time.Millisecond)
close(ready)
}()
var conn net.Conn
if conn, err = listener.Accept(); err != nil {
return fmt.Errorf("error accepting connection: %w", err)
}
n.cfg.Logf("received tcp packet on %q from %q", n.InternalIP, conn.RemoteAddr())
if _, err = conn.Write([]byte("echo\n")); err != nil {
return fmt.Errorf("error writing to tcp socket: %w", err)
}
return nil
}
// StartUDPEchoServer is a helper to run startUDPTCPEchoServer inside a goroutine.
// This function blocks until the server is ready to receive packets or failed
// to start. Errors are sent to the provided channel.
func (n *NamespacePinger) StartUDPEchoServer(errors chan error) {
ready := make(chan struct{})
go func() {
errors <- n.startUDPEchoServer(ready)
}()
<-ready
}
// startUDPEchoServers starts an udp server inside the namespace. The thread
// running the goroutine that process this call will be moved to the namespace.
// This echo servers just returns "echo" as a response. Once one packet is
// received, the server ends. Callers must wait until the ready channel is
// closed before they can start sending packets.
func (n *NamespacePinger) startUDPEchoServer(ready chan struct{}) (err error) {
addr := net.UDPAddr{Port: n.cfg.Port, IP: n.InternalIP}
n.cfg.Logf("starting udp echo server on namespace %q(%q)", n.name, addr.String())
if err = n.Join(); err != nil {
close(ready)
return fmt.Errorf("error joining namespace: %w", err)
}
defer func() {
err = WrapIfFail("error leaving namespace", err, n.Leave)
}()
conn, err := net.ListenUDP("udp", &addr)
if err != nil {
close(ready)
return fmt.Errorf("error starting udp server: %w", err)
}
defer conn.Close()
deadline := time.Now().Add(n.cfg.Timeout)
if err = conn.SetDeadline(deadline); err != nil {
close(ready)
return fmt.Errorf("error setting udp listener deadline: %w", err)
}
go func() {
// XXX: here be dragons. we can't signalize we are ready until
// the call to read is done so we artificially sleep for a bit
// here.
time.Sleep(100 * time.Millisecond)
close(ready)
}()
var source *net.UDPAddr
var buffer = make([]byte, 1024)
if _, source, err = conn.ReadFromUDP(buffer); err != nil {
return fmt.Errorf("error reading from udp socket: %w", err)
}
n.cfg.Logf("received udp packet on %q from %q", n.InternalIP, source.AddrPort())
if _, err = conn.WriteToUDP([]byte("echo"), source); err != nil {
return fmt.Errorf("error writing to udp socket: %w", err)
}
return nil
}
func NewNamespacePinger(name, cidraddr string, options ...Option) (*NamespacePinger, error) {
config := NewConfiguration(options...)
namespace, err := NewManagedNetworkNamespace(name, cidraddr, options...)
if err != nil {
return nil, fmt.Errorf("error creating network namespace: %w", err)
}
return &NamespacePinger{
ManagedNetworkNamespace: namespace,
cfg: config,
}, nil
}