Initial commit: forge-tools-ssh — MCP-сервер для администрирования по SSH
This commit is contained in:
@@ -0,0 +1,609 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user