Skip to content

Commit f316fb6

Browse files
committed
Add kernel MCP tools
Implements MCP tools for inspecting kernel level networking configurations: - get-conntrack: retrieves connection tracking entries from a Kubernetes node. - get-iptables: retrieves iptables/ip6tables rules from a Kubernetes node. - get-nft: retrieves nftables configuration from a Kubernetes node. - get-ip: executes 'ip' utility commands on a node. Signed-off-by: Arnab Ghosh <arnabghosh89@gmail.com>
1 parent de5ea64 commit f316fb6

File tree

13 files changed

+703
-9
lines changed

13 files changed

+703
-9
lines changed

cmd/ovnk-mcp-server/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"syscall"
1212

1313
mcp "github.com/modelcontextprotocol/go-sdk/mcp"
14+
kernelmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/mcp"
1415
kubernetesmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/mcp"
1516
)
1617

@@ -36,6 +37,13 @@ func main() {
3637
}
3738
log.Println("Adding Kubernetes tools to OVN-K MCP server")
3839
k8sMcpServer.AddTools(ovnkMcpServer)
40+
41+
kernelMcpServer, err := kernelmcp.NewMCPServer(serverCfg.Kubernetes)
42+
if err != nil {
43+
log.Fatalf("Failed to create kernel MCP server: %v", err)
44+
}
45+
log.Println("Adding Kernel tools to OVN-K MCP server")
46+
kernelMcpServer.AddTools(ovnkMcpServer)
3947
}
4048

4149
// Create a context that can be cancelled to shutdown the server.

pkg/kernel/mcp/conntrack.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package kernel
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types"
10+
)
11+
12+
const conntrackSystemFile = "/proc/net/nf_conntrack"
13+
14+
// GetConntrack retrieves connection tracking entries from a Kubernetes node. Falls back to /proc/net/nf_conntrack parsing when conntrack CLI unavailable.
15+
// TODO: Add support for conntrack event monitoring (-E flag)
16+
// TODO: Add support for -G (get specific entry) command
17+
func (c *KernelMCPServerClientSet) GetConntrack(ctx context.Context, in types.ListConntrackParams) (string, error) {
18+
conntrack_cli_available := c.UtilityExists(ctx, in.Node, in.Image, "conntrack")
19+
if err := ValidateConntrackCommand(in.Command, conntrack_cli_available); err != nil {
20+
return "", err
21+
}
22+
if !conntrack_cli_available {
23+
stdout, err := c.getConntrackFromFile(ctx, in.Node, in.Image, in.FilterParameters)
24+
if err != nil {
25+
return "", err
26+
}
27+
return stdout, nil
28+
} else {
29+
stdout, err := c.getConntrackUsingCLI(ctx, in.Node, in.Image, in.Command, in.FilterParameters)
30+
if err != nil {
31+
return "", err
32+
}
33+
return stdout, nil
34+
}
35+
}
36+
37+
// getConntrackUsingCLI executes conntrack CLI commands
38+
func (c *KernelMCPServerClientSet) getConntrackUsingCLI(ctx context.Context, node, image, command, filterParameters string) (string, error) {
39+
cmd := newCommand("conntrack")
40+
switch command {
41+
case "-L":
42+
cmd.add("-L", filterParameters)
43+
case "-S":
44+
cmd.add("-S")
45+
case "-C":
46+
cmd.add("-C")
47+
default:
48+
cmd.add("-L")
49+
}
50+
return c.executeCommand(ctx, node, image, cmd.build())
51+
}
52+
53+
// getConntrackFromFile parses /proc/net/nf_conntrack directly
54+
func (c *KernelMCPServerClientSet) getConntrackFromFile(ctx context.Context, node, image, filterParameters string) (string, error) {
55+
var regex string
56+
cmd := newCommand("cat")
57+
if filterParameters != "" {
58+
regex = filterParametersToRegex(filterParameters)
59+
}
60+
if regex != "" {
61+
cmd.add(conntrackSystemFile, "|", "grep", "-E", regex)
62+
} else {
63+
cmd.add(conntrackSystemFile)
64+
}
65+
return c.executeCommand(ctx, node, image, cmd.build())
66+
}
67+
68+
// filterParametersToRegex converts conntrack filter params to grep regex
69+
func filterParametersToRegex(filterParameters string) string {
70+
afterSplit := strings.Split(filterParameters, " ")
71+
var src, dst, protocol, src_port, dst_port string
72+
for i, item := range afterSplit {
73+
if item == "-s" {
74+
src = afterSplit[i+1]
75+
splitSrc := strings.Split(src, ".")
76+
src = "src=" + strings.Join(splitSrc, "\\.")
77+
}
78+
if item == "-d" {
79+
dst = afterSplit[i+1]
80+
splitDst := strings.Split(dst, ".")
81+
dst = "dst=" + strings.Join(splitDst, "\\.")
82+
}
83+
if item == "-p" {
84+
protocol = afterSplit[i+1]
85+
}
86+
if item == "--sport" {
87+
src_port = "sport=" + afterSplit[i+1]
88+
}
89+
if item == "--dport" {
90+
dst_port = "dport=" + afterSplit[i+1]
91+
}
92+
}
93+
return "^.*\\b" + protocol + "\\b.*\\b" + src + "\\s+" + dst + "\\s+" + src_port + "\\s+" + dst_port + "\\b"
94+
}
95+
96+
// ValidateConntrackCommand validates the command to be used to get list of conntrack entries.
97+
// It returns an error if conntrack CLI is not available and any other operation than list is being performed.
98+
func ValidateConntrackCommand(command string, cliAvailable bool) error {
99+
if command == "" {
100+
return nil
101+
}
102+
103+
if !cliAvailable && command != "-L" {
104+
return fmt.Errorf("mentioned image does not have conntrack utility, only -L is supported with limited filters")
105+
}
106+
if _, err := strconv.Atoi(command); err == nil {
107+
return nil
108+
}
109+
validTables := map[string]bool{
110+
"-L": true,
111+
"-S": true,
112+
"-C": true,
113+
}
114+
115+
if !validTables[command] {
116+
return fmt.Errorf("invalid command: %s", command)
117+
}
118+
return nil
119+
}

pkg/kernel/mcp/ip.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package kernel
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types"
10+
)
11+
12+
// GetIPCommandOutput executes 'ip' utility commands on a node. Requires ip utility in the debug container image.
13+
func (c *KernelMCPServerClientSet) GetIPCommandOutput(ctx context.Context, in types.ListIPParams) (string, error) {
14+
ip_cli_available := c.UtilityExists(ctx, in.Node, in.Image, "ip")
15+
if !ip_cli_available {
16+
return "", fmt.Errorf("mentioned image does not have ip utility")
17+
}
18+
if err := ValidateIPCommand(in.Command); err != nil {
19+
return "", err
20+
}
21+
22+
cmd := newCommand("ip")
23+
cmd.addIfNotEmpty(in.Options, in.Options)
24+
cmd.add(strings.Fields(in.Command)...)
25+
cmd.addIfNotEmpty(in.SubCommand, strings.Fields(in.SubCommand)...)
26+
return c.executeCommand(ctx, in.Node, in.Image, cmd.build())
27+
}
28+
29+
// ValidateIPCommand validates that the IP command is allowed.
30+
func ValidateIPCommand(ipCommand string) error {
31+
if ipCommand == "" {
32+
return nil
33+
}
34+
35+
if _, err := strconv.Atoi(ipCommand); err == nil {
36+
return nil
37+
}
38+
validIpCommand := map[string]bool{
39+
"address show": true,
40+
"link show": true,
41+
"neighbour show": true,
42+
"netns show": true,
43+
"route show": true,
44+
"rule show": true,
45+
"vrf show": true,
46+
}
47+
48+
if !validIpCommand[ipCommand] {
49+
return fmt.Errorf("invalid ip command: %s", ipCommand)
50+
}
51+
return nil
52+
}

pkg/kernel/mcp/iptables.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package kernel
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kernel/types"
10+
)
11+
12+
// GetIptables retrieves iptables/ip6tables rules from a Kubernetes node. Automatically detects IPv6 and uses ip6tables when needed.
13+
func (c *KernelMCPServerClientSet) GetIptables(ctx context.Context, in types.ListIPTablesParams) (string, error) {
14+
iptables_cli_available := c.UtilityExists(ctx, in.Node, in.Image, "iptables")
15+
if !iptables_cli_available {
16+
return "", fmt.Errorf("mentioned image does not have iptables utility")
17+
}
18+
if err := ValidateTableName(in.Table); err != nil {
19+
return "", err
20+
}
21+
22+
if err := ValidateIptablesCommand(in.Command); err != nil {
23+
return "", err
24+
}
25+
cmd := newCommand(iptablesCommand(in.FilterParameters))
26+
// Defaults to 'filter' table when not specified
27+
cmd.addIf(in.Table == "", "-t", "filter")
28+
cmd.addIfNotEmpty(in.Table, "-t", in.Table)
29+
// Defaults to -L (list) when command not specified
30+
cmd.addIf(in.Command == "", "-L")
31+
cmd.addIfNotEmpty(in.Command, in.Command)
32+
// FilterParameters are invalid with -S command
33+
cmd.addIf(in.FilterParameters != "" && in.Command != "-S", in.FilterParameters)
34+
return c.executeCommand(ctx, in.Node, in.Image, cmd.build())
35+
}
36+
37+
// iptablesCommand determines whether to use iptables or ip6tables.
38+
func iptablesCommand(filterParameters string) string {
39+
for _, item := range strings.Split(filterParameters, " ") {
40+
if item == "--ipv6" || strings.Contains(item, "6") {
41+
return "ip6tables"
42+
}
43+
}
44+
return "iptables"
45+
}
46+
47+
// ValidateTableName validates iptables table name
48+
func ValidateTableName(table string) error {
49+
if table == "" {
50+
return nil
51+
}
52+
53+
if _, err := strconv.Atoi(table); err == nil {
54+
return nil
55+
}
56+
validTables := map[string]bool{
57+
"filter": true,
58+
"nat": true,
59+
"mangle": true,
60+
"raw": true,
61+
"security": true,
62+
}
63+
64+
if !validTables[table] {
65+
return fmt.Errorf("invalid table name: %s", table)
66+
}
67+
return nil
68+
}
69+
70+
// ValidateIptablesCommand only allow list operation.
71+
func ValidateIptablesCommand(command string) error {
72+
if command == "" {
73+
return nil
74+
}
75+
76+
if _, err := strconv.Atoi(command); err == nil {
77+
return nil
78+
}
79+
validCommand := map[string]bool{
80+
"-L": true,
81+
"-S": true,
82+
}
83+
84+
if !validCommand[command] {
85+
return fmt.Errorf("invalid iptables command: %s", command)
86+
}
87+
return nil
88+
}

0 commit comments

Comments
 (0)