Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions cmd/ovnk-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ import (
kernelmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp"
kubernetesmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/mcp"
mustgathermcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/must-gather/mcp"
nettoolsmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/nettools/mcp"
ovnmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp"
ovsmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovs/mcp"
sosreportmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/sosreport/mcp"
)

type MCPServerConfig struct {
Mode string
Transport string
Port string
Kubernetes kubernetesmcp.Config
Mode string
Transport string
Port string
PwruImage string
TcpdumpImage string
Kubernetes kubernetesmcp.Config
}

// setupLiveCluster sets up the live cluster mode.
Expand All @@ -46,6 +49,10 @@ func setupLiveCluster(serverCfg *MCPServerConfig, server *mcp.Server) {
kernelMcpServer := kernelmcp.NewMCPServer(k8sMcpServer)
log.Println("Adding Kernel tools to OVN-K MCP server")
kernelMcpServer.AddTools(server)

netToolsServer := nettoolsmcp.NewMCPServer(k8sMcpServer, serverCfg.PwruImage, serverCfg.TcpdumpImage)
log.Println("Adding network tools to OVN-K MCP server")
netToolsServer.AddTools(server)
}

// setupOffline sets up the offline mode.
Expand Down Expand Up @@ -140,6 +147,8 @@ func parseFlags() *MCPServerConfig {
flag.StringVar(&cfg.Transport, "transport", "stdio", "Transport to use: stdio or http")
flag.StringVar(&cfg.Port, "port", "8080", "Port to use")
flag.StringVar(&cfg.Kubernetes.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
flag.StringVar(&cfg.PwruImage, "pwru-image", "docker.io/cilium/pwru:v1.0.10", "Container image for pwru operations")
flag.StringVar(&cfg.TcpdumpImage, "tcpdump-image", "nicolaka/netshoot:v0.13", "Container image for tcpdump operations")
flag.Parse()
return cfg
}
39 changes: 26 additions & 13 deletions pkg/kubernetes/client/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (
"k8s.io/utils/ptr"
)

func (c *OVNKMCPServerClientSet) DebugNode(ctx context.Context, name, image string, command []string) (string, string, error) {
func (c *OVNKMCPServerClientSet) DebugNode(ctx context.Context, name, image string, command []string, hostPath, mountPath string) (string, string, error) {
namespace := metav1.NamespaceDefault
debugPodName, cleanupPod, err := c.createPod(ctx, name, namespace, image)
debugPodName, cleanupPod, err := c.createPod(ctx, name, namespace, image, hostPath, mountPath)
if err != nil {
return "", "", err
}
Expand All @@ -32,10 +32,29 @@ func (c *OVNKMCPServerClientSet) DebugNode(ctx context.Context, name, image stri
return stdout, stderr, nil
}

func (c *OVNKMCPServerClientSet) createPod(ctx context.Context, node, namespace, image string) (string, func(), error) {
func (c *OVNKMCPServerClientSet) createPod(ctx context.Context, node, namespace, image, hostPath, mountPath string) (string, func(), error) {
hostPathType := corev1.HostPathDirectory
sleepCommand := []string{"sleep", "infinity"}

if hostPath == "" {
hostPath = "/"
}

if mountPath == "" {
mountPath = "/host"
}

var envVars []corev1.EnvVar
if hostPath == "/" {
// to collect sos report requires this env var is set when hostPath is /
envVars = []corev1.EnvVar{
{
Name: "HOST",
Value: mountPath,
},
}
}

// Create a host networked privileged debug pod.
debugPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -58,7 +77,7 @@ func (c *OVNKMCPServerClientSet) createPod(ctx context.Context, node, namespace,
Name: "host",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/",
Path: hostPath,
Type: &hostPathType,
},
},
Expand All @@ -76,16 +95,10 @@ func (c *OVNKMCPServerClientSet) createPod(ctx context.Context, node, namespace,
VolumeMounts: []corev1.VolumeMount{
{
Name: "host",
MountPath: "/host",
},
},
Env: []corev1.EnvVar{
{
// to collect more sos report requires this env var is set
Name: "HOST",
Value: "/host",
MountPath: mountPath,
},
},
Env: envVars,
}},
},
}
Expand Down Expand Up @@ -115,7 +128,7 @@ func (c *OVNKMCPServerClientSet) createPod(ctx context.Context, node, namespace,
})
if err != nil {
cleanupPod()
return "", nil, fmt.Errorf("debug pod did not reach running state within timeout of 5 minutes: %w", err)
return "", nil, fmt.Errorf("debug pod did not reach running state within timeout of 1 minute: %w", err)
}

return createdDebugPod.Name, cleanupPod, nil
Expand Down
49 changes: 48 additions & 1 deletion pkg/kubernetes/mcp/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,61 @@ package kubernetes

import (
"context"
"fmt"
"path/filepath"
"strings"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types"
)

// validatePath validates that a path is safe to use for mounting.
// It ensures the path:
// - Is absolute (starts with /)
// - Does not contain path traversal patterns (..)
// - Contains only safe characters
func validatePath(path, pathType string) error {
if path == "" {
return nil // Empty paths are allowed and will be set to defaults
}

// Ensure path is absolute
if !filepath.IsAbs(path) {
return fmt.Errorf("%s must be an absolute path (start with /), got: %s", pathType, path)
}

// Check for path traversal patterns: reject any path element that is exactly ".."
for _, elem := range strings.Split(path, string(filepath.Separator)) {
if elem == ".." {
return fmt.Errorf("%s contains path traversal element '..': %s", pathType, path)
}
}

// Check for dangerous characters (null bytes, control characters, shell special characters)
for i, r := range path {
// Allow alphanumeric, /, -, _, ., ~
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') ||
r == '/' || r == '-' || r == '_' || r == '.' || r == '~' {
continue
}
return fmt.Errorf("%s contains unsafe character at position %d: %c (U+%04X)", pathType, i, r, r)
}

return nil
}

// DebugNode debugs a node by name, image and command.
func (s *MCPServer) DebugNode(ctx context.Context, req *mcp.CallToolRequest, in types.DebugNodeParams) (*mcp.CallToolResult, types.DebugNodeResult, error) {
stdout, stderr, err := s.clientSet.DebugNode(ctx, in.Name, in.Image, in.Command)
// Validate paths before creating the pod
if err := validatePath(in.HostPath, "hostPath"); err != nil {
return nil, types.DebugNodeResult{}, err
}

if err := validatePath(in.MountPath, "mountPath"); err != nil {
return nil, types.DebugNodeResult{}, err
}

stdout, stderr, err := s.clientSet.DebugNode(ctx, in.Name, in.Image, in.Command, in.HostPath, in.MountPath)
if err != nil {
return nil, types.DebugNodeResult{}, err
}
Expand Down
8 changes: 5 additions & 3 deletions pkg/kubernetes/types/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package types

// DebugNodeParams is a type that contains the name, image and command of a node.
type DebugNodeParams struct {
Name string `json:"name"`
Image string `json:"image"`
Command []string `json:"command"`
Name string `json:"name"`
Image string `json:"image"`
Command []string `json:"command"`
HostPath string `json:"hostPath,omitempty"`
MountPath string `json:"mountPath,omitempty"`
Comment on lines +5 to +9
Copy link

@coderabbitai coderabbitai bot Jan 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's view the struct definition in the types.go file
cat -n pkg/kubernetes/types/nodes.go | head -30

Repository: ovn-kubernetes/ovn-kubernetes-mcp

Length of output: 705


🏁 Script executed:

# Search for usages of HostPath and MountPath in the codebase (excluding test files and the types file itself)
rg -n 'HostPath|MountPath' --type=go -g '!*test*.go' -g '!*_test.go' | head -50

Repository: ovn-kubernetes/ovn-kubernetes-mcp

Length of output: 7511


🏁 Script executed:

# Also search for any validation functions that might handle these paths
rg -n 'ValidatePath|SanitizePath|ValidateMount|PathTraversal' --type=go

Repository: ovn-kubernetes/ovn-kubernetes-mcp

Length of output: 318


🏁 Script executed:

# Search for DebugNodeParams usage (excluding vendor)
rg -n 'DebugNodeParams' --type=go -g '!vendor/**'

Repository: ovn-kubernetes/ovn-kubernetes-mcp

Length of output: 1033


🏁 Script executed:

# Also search for files that might use HostPath or MountPath in the context of debugging
rg -n 'HostPath|MountPath' --type=go -g '!vendor/**' -g '!*test*.go'

Repository: ovn-kubernetes/ovn-kubernetes-mcp

Length of output: 732


🏁 Script executed:

# Search for where the debug node feature is implemented
rg -n 'debug.*node|DebugNode' --type=go -i -g '!vendor/**'

Repository: ovn-kubernetes/ovn-kubernetes-mcp

Length of output: 2598


🏁 Script executed:

# View the nodes.go client file to see how HostPath and MountPath are used
cat -n pkg/kubernetes/client/nodes.go

Repository: ovn-kubernetes/ovn-kubernetes-mcp

Length of output: 4508


🏁 Script executed:

# Check the pwru.go file where HostPath and MountPath are set
cat -n pkg/external_tools/mcp/pwru.go | head -50

Repository: ovn-kubernetes/ovn-kubernetes-mcp

Length of output: 2048


Add path validation before creating debug pods.

The createPod function in pkg/kubernetes/client/nodes.go accepts hostPath and mountPath without validation. While it provides default values for empty strings (lines 39-45), there is no validation to prevent path traversal attacks (e.g., paths containing ..) or ensure paths are absolute. The values are passed directly to Kubernetes API calls (lines 79-80, 98) without sanitization.

Add validation to ensure:

  • Both paths are absolute (start with /)
  • Neither path contains .. or other traversal patterns
  • Paths contain only safe characters

This is particularly important since the fields are optional and part of the public API, making them susceptible to misuse by future callers beyond the current hardcoded usage in pwru.go.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Meina-rh this is a good suggestion. Let's add this validation in pkg/kubernetes/mcp/nodes.go.

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@arkadeepsen added, please take a look if it's right.

}

// DebugNodeResult is a type that contains the stdout and stderr of the executed command.
Expand Down
76 changes: 76 additions & 0 deletions pkg/nettools/mcp/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package nettools

import (
"github.com/modelcontextprotocol/go-sdk/mcp"
kubernetesmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/mcp"
)

// MCPServer provides MCP server functionality for network tools operations.
type MCPServer struct {
k8sMcpServer *kubernetesmcp.MCPServer
pwruImage string
tcpdumpImage string
}

// NewMCPServer creates a new MCP server instance
func NewMCPServer(k8sMcpServer *kubernetesmcp.MCPServer, pwruImage, tcpdumpImage string) *MCPServer {
return &MCPServer{
k8sMcpServer: k8sMcpServer,
pwruImage: pwruImage,
tcpdumpImage: tcpdumpImage,
}
}

// AddTools registers network tools with the MCP server
func (s *MCPServer) AddTools(server *mcp.Server) {
mcp.AddTool(server,
&mcp.Tool{
Name: "tcpdump",
Description: `Capture network packets on a node or inside a pod with strict safety controls.

Supports both node-level and pod-level packet capture with BPF filtering.

This tool creates a specialized debug pod on the specified node for node-level captures.
The node image can be configured via --tcpdump-image flag or will default to nicolaka/netshoot:v0.13.
The image must contain the tcpdump utility

Parameters:
- target_type: 'node' or 'pod' (required)
- node_name: Name of the node (required when target_type is 'node')
- pod_name: Name of the pod (required when target_type is 'pod')
- pod_namespace: Namespace of the pod (required when target_type is 'pod')
- container_name: Name of the container in the pod (optional, uses default container if not specified)
- interface: Network interface name or 'any' (optional, uses default if not specified)
- packet_count: Number of packets to capture (default: 100, max: 1000)
- bpf_filter: BPF filter expression to match packets (optional, e.g., "tcp and dst port 8080", "host 10.0.0.1")
- snaplen: Snapshot length in bytes (default: 96, max: 1500)

Examples:
- Capture on node: {"target_type": "node", "node_name": "worker-1", "interface": "eth0", "packet_count": 100, "bpf_filter": "tcp port 80"}
- Capture in pod: {"target_type": "pod", "pod_name": "my-pod", "pod_namespace": "default", "interface": "eth0", "packet_count": 100, "bpf_filter": "host 10.0.0.1"}
- Capture DNS: {"target_type": "node", "node_name": "worker-1", "interface": "any", "packet_count": 50, "bpf_filter": "port 53"}`,
}, s.Tcpdump)
mcp.AddTool(server,
&mcp.Tool{
Name: "pwru",
Description: `Trace packets through the Linux kernel networking stack using eBPF.

pwru (packet, where are you?) shows which kernel functions process a packet, helping debug packet
drops, routing issues, and understanding the kernel's packet processing path.

This tool creates a specialized debug pod on the specified node with necessary eBPF capabilities
to trace packets through kernel networking functions.
The node image can be configured via --pwru-image flag or will default to docker.io/cilium/pwru:v1.0.10.
The image must contain the pwru utility

Parameters:
- node_name: Name of the node to run pwru on (required)
- bpf_filter: BPF filter expression to match packets (optional, e.g., "tcp and dst port 8080", "host 10.0.0.1")
- output_limit_lines: Maximum number of trace events to capture (default: 100, max: 1000)

Examples:
- Basic trace: {"node_name": "worker-1", "bpf_filter": "host 10.244.0.5", "output_limit_lines": 100}
- TCP traffic: {"node_name": "worker-1", "bpf_filter": "tcp and dst port 8080", "output_limit_lines": 50}
- ICMP packets: {"node_name": "worker-1", "bpf_filter": "icmp", "output_limit_lines": 100}`,
}, s.Pwru)
}
50 changes: 50 additions & 0 deletions pkg/nettools/mcp/pwru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package nettools

import (
"context"
"strconv"

"github.com/modelcontextprotocol/go-sdk/mcp"
k8stypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types"
"github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/nettools/types"
)

const (
DefaultOutputLimitLines = 100
MaxOutputLimitLines = 1000
)

// Pwru executes the pwru (packet, where are you?) tool to trace packets through the Linux kernel.
// It creates a specialized debug pod with eBPF capabilities and traces packet processing paths.
// This is useful for debugging packet drops, routing issues, and understanding kernel networking behavior.
func (s *MCPServer) Pwru(ctx context.Context, req *mcp.CallToolRequest, in types.PwruParams) (*mcp.CallToolResult, types.CommandResult, error) {
outputLimitLines := in.OutputLimitLines
if outputLimitLines == 0 {
outputLimitLines = DefaultOutputLimitLines
}
if err := validateIntMax(outputLimitLines, MaxOutputLimitLines, "output_limit_lines", ""); err != nil {
return nil, types.CommandResult{}, err
}

if err := validatePacketFilter(in.BPFFilter); err != nil {
return nil, types.CommandResult{}, err
}

cmd := newCommand("pwru", "--output-limit-lines", strconv.Itoa(outputLimitLines))
// pwru accepts pcap filter as positional argument(s)
cmd.addIfNotEmpty(in.BPFFilter, in.BPFFilter)

target := k8stypes.DebugNodeParams{
Name: in.NodeName,
Image: s.pwruImage,
HostPath: "/sys/kernel/debug",
MountPath: "/sys/kernel/debug",
Command: cmd.build(),
}

result, err := s.runDebugNode(ctx, req, target)
if err != nil {
return nil, types.CommandResult{}, err
}
return nil, result, nil
}
Loading
Loading