610 lines
22 KiB
Go
610 lines
22 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// VoIP constants
|
|
const (
|
|
SIPUDPPort = 5060
|
|
SIPTCPPort = 5060
|
|
SIPTLSPort = 5061
|
|
RTPPortRange = "50000-60000"
|
|
DefaultPCAPLimit = 5 * 1024 * 1024
|
|
)
|
|
|
|
func registerVoIPTools(s *mcp.Server) {
|
|
// voip_discover_containers
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "voip_discover_containers",
|
|
Description: "Find VoIP-related containers by name/image keywords",
|
|
InputSchema: schema(map[string]any{
|
|
"keywords": arrayStringProps("Keywords to match (default: gw, media, fs, sbc, sw)", false),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, nil),
|
|
}, voipDiscoverHandler)
|
|
|
|
// voip_sip_capture
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "voip_sip_capture",
|
|
Description: "Capture SIP signaling to PCAP using sngrep inside container",
|
|
InputSchema: schema(map[string]any{
|
|
"container": strProps("Container name", true),
|
|
"duration": intProps("Capture duration in seconds (default: 30)", false),
|
|
"port": intProps("SIP port to filter (default: 5060)", false),
|
|
"protocol": strProps("Protocol filter (default: all)", false, "udp", "tcp", "tls"),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, []string{"container"}),
|
|
}, sipCaptureHandler)
|
|
|
|
// voip_call_flow
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "voip_call_flow",
|
|
Description: "Parse SIP call flow from a PCAP file",
|
|
InputSchema: schema(map[string]any{
|
|
"container": strProps("Container name", true),
|
|
"pcap_file": strProps("Path to PCAP file in container", true),
|
|
"call_id": strProps("Filter by Call-ID", false),
|
|
"phone_number": strProps("Filter by phone number", false),
|
|
"summary_only": boolProps("Return summary only, no message details", false),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, []string{"container", "pcap_file"}),
|
|
}, callFlowHandler)
|
|
|
|
// voip_registrations
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "voip_registrations",
|
|
Description: "Extract REGISTER dialogs and outcomes from SIP PCAP",
|
|
InputSchema: schema(map[string]any{
|
|
"container": strProps("Container name", true),
|
|
"pcap_file": strProps("Path to PCAP file", true),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, []string{"container", "pcap_file"}),
|
|
}, registrationsHandler)
|
|
|
|
// voip_call_stats
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "voip_call_stats",
|
|
Description: "Aggregate SIP call statistics from PCAP",
|
|
InputSchema: schema(map[string]any{
|
|
"container": strProps("Container name", true),
|
|
"pcap_file": strProps("Path to PCAP file", true),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, []string{"container", "pcap_file"}),
|
|
}, callStatsHandler)
|
|
|
|
// voip_extract_sdp
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "voip_extract_sdp",
|
|
Description: "Extract SDP (codecs, RTP ports) from SIP messages",
|
|
InputSchema: schema(map[string]any{
|
|
"container": strProps("Container name", true),
|
|
"pcap_file": strProps("Path to PCAP file", true),
|
|
"call_id": strProps("Filter by specific Call-ID", false),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, []string{"container", "pcap_file"}),
|
|
}, extractSDPHandler)
|
|
|
|
// voip_packet_check
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "voip_packet_check",
|
|
Description: "Quick SIP packet presence check on standard ports",
|
|
InputSchema: schema(map[string]any{
|
|
"container": strProps("Container name", true),
|
|
"duration": intProps("Check duration in seconds (default: 5)", false),
|
|
"interface": strProps("Network interface (default: any)", false),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, []string{"container"}),
|
|
}, packetCheckHandler)
|
|
|
|
// voip_network_capture
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "voip_network_capture",
|
|
Description: "Capture SIP packets with tcpdump for analysis",
|
|
InputSchema: schema(map[string]any{
|
|
"container": strProps("Container name", true),
|
|
"duration": intProps("Capture duration in seconds (default: 30)", false),
|
|
"interface": strProps("Network interface (default: any)", false),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, []string{"container"}),
|
|
}, networkCaptureHandler)
|
|
|
|
// voip_rtp_capture
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "voip_rtp_capture",
|
|
Description: "Capture RTP packets to verify media flow",
|
|
InputSchema: schema(map[string]any{
|
|
"container": strProps("Container name", true),
|
|
"duration": intProps("Capture duration in seconds (default: 10)", false),
|
|
"port_range": strProps("RTP port range (default: 50000-60000)", false),
|
|
"interface": strProps("Network interface (default: any)", false),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, []string{"container"}),
|
|
}, rtpCaptureHandler)
|
|
|
|
// voip_network_diagnostics
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "voip_network_diagnostics",
|
|
Description: "Run network diagnostics: ping, traceroute, TCP port checks",
|
|
InputSchema: schema(map[string]any{
|
|
"host": strProps("Target host for diagnostics", true),
|
|
"ports": arrayNumberProps("TCP ports to check (default: 5060, 5061)", false),
|
|
"ping_count": intProps("Number of pings (default: 3)", false),
|
|
"traceroute": boolProps("Include traceroute (default: true)", false),
|
|
"timeout": intProps("Timeout in seconds (default: 15)", false),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, []string{"host"}),
|
|
}, networkDiagnosticsHandler)
|
|
}
|
|
|
|
func voipDiscoverHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
args := requestArgs(req)
|
|
target := getString(args, "target", "primary")
|
|
|
|
if err := checkDockerAvailable(ctx, m, target); err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
// Default VoIP keywords
|
|
defaultKeywords := []string{"gw", "media", "fs", "sbc", "sw", "freeswitch", "asterisk", "kamailio", "opensips", "rtpengine"}
|
|
keywords := defaultKeywords
|
|
|
|
if kws, ok := getStringArray(args, "keywords"); ok {
|
|
var sanitized []string
|
|
for _, kw := range kws {
|
|
if safe := sanitizeAlphanumeric(kw); safe != "" {
|
|
sanitized = append(sanitized, safe)
|
|
}
|
|
}
|
|
if len(sanitized) > 0 {
|
|
keywords = sanitized
|
|
}
|
|
}
|
|
|
|
pattern := strings.Join(keywords, "|")
|
|
cmd := fmt.Sprintf(`docker ps --format '{{.Names}}|{{.Image}}' | grep -iE %s 2>/dev/null || echo ''`, shellQuote(pattern))
|
|
|
|
output, err := m.Execute(ctx, cmd, target)
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
if trimOutput(output) == "" {
|
|
return textResult("No VoIP containers found"), nil
|
|
}
|
|
|
|
var containers []map[string]string
|
|
for _, line := range strings.Split(output, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(line, "|", 2)
|
|
if len(parts) == 2 {
|
|
containers = append(containers, map[string]string{
|
|
"name": parts[0],
|
|
"image": parts[1],
|
|
})
|
|
}
|
|
}
|
|
jsonBytes, _ := json.MarshalIndent(containers, "", " ")
|
|
return textResult(string(jsonBytes)), nil
|
|
}
|
|
|
|
func sipCaptureHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
args := requestArgs(req)
|
|
container, _ := requireString(args, "container")
|
|
duration := getInt(args, "duration", 30)
|
|
port := getInt(args, "port", 0)
|
|
protocol := getString(args, "protocol", "")
|
|
target := getString(args, "target", "primary")
|
|
|
|
if err := checkDockerAvailable(ctx, m, target); err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
checkCmd := fmt.Sprintf("docker exec %s command -v sngrep >/dev/null 2>&1 && echo 'ok' || echo 'missing'", shellQuote(container))
|
|
checkOutput, err := m.Execute(ctx, checkCmd, target)
|
|
if err != nil || !containsString(checkOutput, "ok") {
|
|
return errorResult("sngrep not available in container. Install with: apt-get install sngrep"), nil
|
|
}
|
|
|
|
bpfFilter := buildSIPFilter(port, protocol)
|
|
pcapPath := fmt.Sprintf("/tmp/voip_sip_%d.pcap", time.Now().Unix())
|
|
|
|
cmd := fmt.Sprintf("docker exec %s timeout %ds sngrep -N -q -d any -O %s '%s' 2>&1 || true",
|
|
shellQuote(container), duration, shellQuote(pcapPath), bpfFilter)
|
|
m.Execute(ctx, cmd, target)
|
|
|
|
checkFile := fmt.Sprintf("docker exec %s test -f %s && echo 'exists' || echo 'missing'", shellQuote(container), shellQuote(pcapPath))
|
|
checkResult, _ := m.Execute(ctx, checkFile, target)
|
|
fileStatus := "created"
|
|
if !containsString(checkResult, "exists") {
|
|
fileStatus = "not created (capture may have failed)"
|
|
}
|
|
|
|
result := map[string]any{
|
|
"container": container,
|
|
"pcap_file": pcapPath,
|
|
"duration": duration,
|
|
"filter": bpfFilter,
|
|
"file_status": fileStatus,
|
|
"message": fmt.Sprintf("SIP capture completed. Use voip_call_flow to analyze %s", pcapPath),
|
|
}
|
|
jsonBytes, _ := json.MarshalIndent(result, "", " ")
|
|
return textResult(string(jsonBytes)), nil
|
|
}
|
|
|
|
func callFlowHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
args := requestArgs(req)
|
|
container, _ := requireString(args, "container")
|
|
rawPcapFile, _ := requireString(args, "pcap_file")
|
|
callID := getString(args, "call_id", "")
|
|
phoneNumber := getString(args, "phone_number", "")
|
|
summaryOnly := getBool(args, "summary_only", false)
|
|
target := getString(args, "target", "primary")
|
|
|
|
pcapFile, err := sanitizeShellInnerPath(rawPcapFile)
|
|
if err != nil {
|
|
return errorResult("invalid pcap_file path"), nil
|
|
}
|
|
if err := checkDockerAvailable(ctx, m, target); err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
var filter string
|
|
if callID != "" {
|
|
filter = fmt.Sprintf("-Y 'sip.Call-ID == \"%s\"'", sanitizeTsharkValue(callID))
|
|
} else if phoneNumber != "" {
|
|
filter = fmt.Sprintf("-Y 'sip contains \"%s\"'", sanitizeTsharkValue(phoneNumber))
|
|
}
|
|
|
|
quotedPcap := shellQuote(pcapFile)
|
|
var cmd string
|
|
if summaryOnly {
|
|
cmd = fmt.Sprintf(`docker exec %s sh -c 'if command -v tshark >/dev/null 2>&1; then tshark -r %s -T fields -e frame.time -e ip.src -e ip.dst -e sip.Method -e sip.Status-Code -e sip.Call-ID %s 2>/dev/null | head -100; else sngrep -I %s -q 2>/dev/null | head -50 || echo "No analysis tool available"; fi'`,
|
|
shellQuote(container), quotedPcap, filter, quotedPcap)
|
|
} else {
|
|
cmd = fmt.Sprintf(`docker exec %s sh -c 'if command -v tshark >/dev/null 2>&1; then tshark -r %s -V -Y sip %s 2>/dev/null | head -500; else cat %s 2>/dev/null | strings | grep -E "^(INVITE|REGISTER|BYE|ACK|CANCEL|SIP/2.0)" | head -100 || echo "No analysis tool available"; fi'`,
|
|
shellQuote(container), quotedPcap, filter, quotedPcap)
|
|
}
|
|
|
|
output, err := m.Execute(ctx, cmd, target)
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
return textResult(output), nil
|
|
}
|
|
|
|
func registrationsHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
args := requestArgs(req)
|
|
container, _ := requireString(args, "container")
|
|
rawPcapFile, _ := requireString(args, "pcap_file")
|
|
target := getString(args, "target", "primary")
|
|
|
|
pcapFile, err := sanitizeShellInnerPath(rawPcapFile)
|
|
if err != nil {
|
|
return errorResult("invalid pcap_file path"), nil
|
|
}
|
|
if err := checkDockerAvailable(ctx, m, target); err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
quotedPcap := shellQuote(pcapFile)
|
|
cmd := fmt.Sprintf(`docker exec %s sh -c 'if command -v tshark >/dev/null 2>&1; then tshark -r %s -Y "sip.Method == REGISTER or (sip.CSeq.method == REGISTER and sip.Status-Code)" -T fields -e frame.time -e sip.from.user -e sip.to.user -e sip.contact.uri -e sip.Status-Code -E header=y 2>/dev/null; else cat %s 2>/dev/null | strings | grep -E "(REGISTER|200 OK|401|403)" | head -50; fi'`,
|
|
shellQuote(container), quotedPcap, quotedPcap)
|
|
|
|
output, err := m.Execute(ctx, cmd, target)
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
return textResult(output), nil
|
|
}
|
|
|
|
func callStatsHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
args := requestArgs(req)
|
|
container, _ := requireString(args, "container")
|
|
rawPcapFile, _ := requireString(args, "pcap_file")
|
|
target := getString(args, "target", "primary")
|
|
|
|
pcapFile, err := sanitizeShellInnerPath(rawPcapFile)
|
|
if err != nil {
|
|
return errorResult("invalid pcap_file path"), nil
|
|
}
|
|
if err := checkDockerAvailable(ctx, m, target); err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
quotedPcap := shellQuote(pcapFile)
|
|
cmd := fmt.Sprintf(`docker exec %s sh -c '
|
|
if command -v tshark >/dev/null 2>&1; then
|
|
echo "=== SIP STATISTICS ==="
|
|
echo ""
|
|
echo "--- Request Methods ---"
|
|
tshark -r %s -Y sip.Method -T fields -e sip.Method 2>/dev/null | sort | uniq -c | sort -rn
|
|
echo ""
|
|
echo "--- Response Codes ---"
|
|
tshark -r %s -Y sip.Status-Code -T fields -e sip.Status-Code 2>/dev/null | sort | uniq -c | sort -rn
|
|
echo ""
|
|
echo "--- Unique Call-IDs ---"
|
|
tshark -r %s -Y sip -T fields -e sip.Call-ID 2>/dev/null | sort -u | wc -l | xargs echo "Total calls:"
|
|
else
|
|
cat %s 2>/dev/null | strings | grep -oE "^(INVITE|REGISTER|BYE|ACK|CANCEL|OPTIONS|SIP/2.0 [0-9]+)" | sort | uniq -c | sort -rn
|
|
fi'`, shellQuote(container), quotedPcap, quotedPcap, quotedPcap, quotedPcap)
|
|
|
|
output, err := m.Execute(ctx, cmd, target)
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
return textResult(output), nil
|
|
}
|
|
|
|
func extractSDPHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
args := requestArgs(req)
|
|
container, _ := requireString(args, "container")
|
|
rawPcapFile, _ := requireString(args, "pcap_file")
|
|
callID := getString(args, "call_id", "")
|
|
target := getString(args, "target", "primary")
|
|
|
|
pcapFile, err := sanitizeShellInnerPath(rawPcapFile)
|
|
if err != nil {
|
|
return errorResult("invalid pcap_file path"), nil
|
|
}
|
|
if err := checkDockerAvailable(ctx, m, target); err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
var filter string
|
|
if callID != "" {
|
|
filter = fmt.Sprintf("-Y 'sip.Call-ID == \"%s\" and sdp'", sanitizeTsharkValue(callID))
|
|
} else {
|
|
filter = "-Y 'sdp'"
|
|
}
|
|
|
|
quotedPcap := shellQuote(pcapFile)
|
|
cmd := fmt.Sprintf(`docker exec %s sh -c 'if command -v tshark >/dev/null 2>&1; then tshark -r %s %s -T fields -e sdp.connection_info -e sdp.media -e sdp.media.port -e sdp.media.format -E header=y 2>/dev/null | head -50; else cat %s 2>/dev/null | strings | grep -E "^(c=|m=|a=rtpmap)" | head -50; fi'`,
|
|
shellQuote(container), quotedPcap, filter, quotedPcap)
|
|
|
|
output, err := m.Execute(ctx, cmd, target)
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
return textResult(output), nil
|
|
}
|
|
|
|
func packetCheckHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
args := requestArgs(req)
|
|
container, _ := requireString(args, "container")
|
|
duration := getInt(args, "duration", 5)
|
|
iface := sanitizeAlphanumeric(getString(args, "interface", "any"))
|
|
target := getString(args, "target", "primary")
|
|
|
|
if iface == "" {
|
|
iface = "any"
|
|
}
|
|
if duration < 1 || duration > 300 {
|
|
duration = 5
|
|
}
|
|
if err := checkDockerAvailable(ctx, m, target); err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
cmd := fmt.Sprintf(`docker exec %s sh -c 'if command -v tcpdump >/dev/null 2>&1; then timeout %ds tcpdump -i %s -c 20 port 5060 or port 5061 2>&1 | tail -25; else echo "tcpdump not available"; fi'`,
|
|
shellQuote(container), duration, shellQuote(iface))
|
|
|
|
output, err := m.Execute(ctx, cmd, target)
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
hasPackets := containsString(output, "UDP") || containsString(output, "TCP") || containsString(output, "SIP")
|
|
summary := "SIP packets detected: NO"
|
|
if hasPackets {
|
|
summary = "SIP packets detected: YES"
|
|
}
|
|
return textResult(fmt.Sprintf("%s\n\n%s", summary, output)), nil
|
|
}
|
|
|
|
func networkCaptureHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
args := requestArgs(req)
|
|
container, _ := requireString(args, "container")
|
|
duration := getInt(args, "duration", 30)
|
|
iface := sanitizeAlphanumeric(getString(args, "interface", "any"))
|
|
target := getString(args, "target", "primary")
|
|
|
|
if iface == "" {
|
|
iface = "any"
|
|
}
|
|
if duration < 1 || duration > 300 {
|
|
duration = 30
|
|
}
|
|
if err := checkDockerAvailable(ctx, m, target); err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
pcapPath := fmt.Sprintf("/tmp/voip_net_%d.pcap", time.Now().Unix())
|
|
cmd := fmt.Sprintf(`docker exec %s sh -c 'if command -v tcpdump >/dev/null 2>&1; then timeout %ds tcpdump -i %s -w %s port 5060 or port 5061 2>&1 || true; else echo "tcpdump not available"; fi'`,
|
|
shellQuote(container), duration, shellQuote(iface), shellQuote(pcapPath))
|
|
m.Execute(ctx, cmd, target)
|
|
|
|
checkFile := fmt.Sprintf("docker exec %s test -f %s && echo 'exists' || echo 'missing'", shellQuote(container), shellQuote(pcapPath))
|
|
checkResult, _ := m.Execute(ctx, checkFile, target)
|
|
fileStatus := "created"
|
|
if !containsString(checkResult, "exists") {
|
|
fileStatus = "not created (capture may have failed)"
|
|
}
|
|
|
|
result := map[string]any{
|
|
"container": container,
|
|
"pcap_file": pcapPath,
|
|
"duration": duration,
|
|
"interface": iface,
|
|
"file_status": fileStatus,
|
|
"message": "Network capture complete. Analyze with voip_call_flow or copy with docker_cp_from",
|
|
}
|
|
jsonBytes, _ := json.MarshalIndent(result, "", " ")
|
|
return textResult(string(jsonBytes)), nil
|
|
}
|
|
|
|
func rtpCaptureHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
args := requestArgs(req)
|
|
container, _ := requireString(args, "container")
|
|
duration := getInt(args, "duration", 10)
|
|
portRange := getString(args, "port_range", RTPPortRange)
|
|
iface := sanitizeAlphanumeric(getString(args, "interface", "any"))
|
|
target := getString(args, "target", "primary")
|
|
|
|
if iface == "" {
|
|
iface = "any"
|
|
}
|
|
if duration < 1 || duration > 300 {
|
|
duration = 10
|
|
}
|
|
if err := checkDockerAvailable(ctx, m, target); err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
startPort := 50000
|
|
endPort := 60000
|
|
ports := strings.Split(portRange, "-")
|
|
if len(ports) == 2 {
|
|
if sp, err := strconv.Atoi(strings.TrimSpace(ports[0])); err == nil && sp > 0 && sp <= 65535 {
|
|
startPort = sp
|
|
}
|
|
if ep, err := strconv.Atoi(strings.TrimSpace(ports[1])); err == nil && ep > 0 && ep <= 65535 {
|
|
endPort = ep
|
|
}
|
|
}
|
|
|
|
cmd := fmt.Sprintf(`docker exec %s sh -c 'if command -v tcpdump >/dev/null 2>&1; then timeout %ds tcpdump -i %s -c 100 "udp portrange %d-%d" 2>&1 | tail -20; else echo "tcpdump not available"; fi'`,
|
|
shellQuote(container), duration, shellQuote(iface), startPort, endPort)
|
|
|
|
output, err := m.Execute(ctx, cmd, target)
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
hasRTP := containsString(output, "UDP") && containsString(output, "packets")
|
|
summary := "RTP packets detected: NO"
|
|
if hasRTP {
|
|
summary = "RTP packets detected: YES"
|
|
}
|
|
return textResult(fmt.Sprintf("%s\nPort range: %s\n\n%s", summary, portRange, output)), nil
|
|
}
|
|
|
|
func networkDiagnosticsHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
args := requestArgs(req)
|
|
host, _ := requireString(args, "host")
|
|
pingCount := getInt(args, "ping_count", 3)
|
|
doTraceroute := getBool(args, "traceroute", true)
|
|
timeout := getInt(args, "timeout", 15)
|
|
target := getString(args, "target", "primary")
|
|
|
|
ports := []int{5060, 5061}
|
|
if ps, ok := getIntArray(args, "ports"); ok {
|
|
ports = ps
|
|
}
|
|
|
|
var sb strings.Builder
|
|
fmt.Fprintf(&sb, "=== NETWORK DIAGNOSTICS: %s ===\n\n", host)
|
|
|
|
sb.WriteString("--- PING ---\n")
|
|
pingCmd := fmt.Sprintf("ping -c %d -W 3 %s 2>&1 || echo 'Ping failed'", pingCount, shellQuote(host))
|
|
pingOutput, _ := m.Execute(ctx, pingCmd, target)
|
|
sb.WriteString(pingOutput)
|
|
sb.WriteString("\n\n")
|
|
|
|
if doTraceroute {
|
|
sb.WriteString("--- TRACEROUTE ---\n")
|
|
traceCmd := fmt.Sprintf("timeout %ds traceroute -m 15 %s 2>&1 || tracepath %s 2>&1 || echo 'Traceroute not available'",
|
|
timeout, shellQuote(host), shellQuote(host))
|
|
traceOutput, _ := m.Execute(ctx, traceCmd, target)
|
|
sb.WriteString(traceOutput)
|
|
sb.WriteString("\n\n")
|
|
}
|
|
|
|
sb.WriteString("--- TCP PORT CHECKS ---\n")
|
|
for _, port := range ports {
|
|
checkCmd := fmt.Sprintf("timeout 3 bash -c 'echo >/dev/tcp/%s/%d' 2>&1 && echo 'Port %d: OPEN' || echo 'Port %d: CLOSED/FILTERED'",
|
|
shellQuote(host), port, port, port)
|
|
checkOutput, _ := m.Execute(ctx, checkCmd, target)
|
|
fmt.Fprintf(&sb, "%s\n", trimOutput(checkOutput))
|
|
}
|
|
|
|
return textResult(sb.String()), nil
|
|
}
|
|
|
|
// buildSIPFilter создаёт BPF-фильтр для SIP-трафика.
|
|
func buildSIPFilter(port int, protocol string) string {
|
|
if protocol != "" {
|
|
proto := strings.ToLower(protocol)
|
|
switch proto {
|
|
case "tls":
|
|
if port == 0 {
|
|
port = SIPTLSPort
|
|
}
|
|
return fmt.Sprintf("tcp port %d", port)
|
|
case "tcp":
|
|
if port == 0 {
|
|
port = SIPTCPPort
|
|
}
|
|
return fmt.Sprintf("tcp port %d", port)
|
|
case "udp":
|
|
if port == 0 {
|
|
port = SIPUDPPort
|
|
}
|
|
return fmt.Sprintf("udp port %d", port)
|
|
}
|
|
}
|
|
if port != 0 {
|
|
return fmt.Sprintf("udp port %d or tcp port %d", port, port)
|
|
}
|
|
return "udp port 5060 or tcp port 5060 or tcp port 5061"
|
|
}
|