diff --git a/agent/app/api/v2/entry.go b/agent/app/api/v2/entry.go index a919fe6f70e6..4b11839e36fb 100644 --- a/agent/app/api/v2/entry.go +++ b/agent/app/api/v2/entry.go @@ -77,5 +77,6 @@ var ( groupService = service.NewIGroupService() alertService = service.NewIAlertService() - diskService = service.NewIDiskService() + diskService = service.NewIDiskService() + toolboxNetService = service.NewIToolboxNetService() ) diff --git a/agent/app/api/v2/toolbox_net.go b/agent/app/api/v2/toolbox_net.go new file mode 100644 index 000000000000..704b41acd99f --- /dev/null +++ b/agent/app/api/v2/toolbox_net.go @@ -0,0 +1,42 @@ +package v2 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/gin-gonic/gin" +) + +// @Tags Toolbox +// @Summary Run network diagnostic tool +// @Accept json +// @Param request body dto.NetToolReq true "request" +// @Success 200 {object} dto.NetToolRes +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /toolbox/net [post] +func (b *BaseApi) RunNetTool(c *gin.Context) { + var req dto.NetToolReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + var output string + var err error + + switch req.Tool { + case "ping": + output, err = toolboxNetService.Ping(req) + case "traceroute": + output, err = toolboxNetService.Traceroute(req) + case "dns": + output, err = toolboxNetService.DNSLookup(req) + case "http": + output, err = toolboxNetService.HTTPCheck(req) + } + + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, dto.NetToolRes{Output: output}) +} diff --git a/agent/app/dto/toolbox.go b/agent/app/dto/toolbox.go new file mode 100644 index 000000000000..2582ce90af5c --- /dev/null +++ b/agent/app/dto/toolbox.go @@ -0,0 +1,11 @@ +package dto + +type NetToolReq struct { + Tool string `json:"tool" validate:"required,oneof=ping traceroute dns http"` + Host string `json:"host" validate:"required"` + Count int `json:"count"` +} + +type NetToolRes struct { + Output string `json:"output"` +} diff --git a/agent/app/service/toolbox_net.go b/agent/app/service/toolbox_net.go new file mode 100644 index 000000000000..8571a440ea5e --- /dev/null +++ b/agent/app/service/toolbox_net.go @@ -0,0 +1,174 @@ +package service + +import ( + "context" + "fmt" + "net" + "net/http" + "os/exec" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +type ToolboxNetService struct{} + +type IToolboxNetService interface { + Ping(req dto.NetToolReq) (string, error) + Traceroute(req dto.NetToolReq) (string, error) + DNSLookup(req dto.NetToolReq) (string, error) + HTTPCheck(req dto.NetToolReq) (string, error) +} + +func NewIToolboxNetService() IToolboxNetService { + return &ToolboxNetService{} +} + +func (t *ToolboxNetService) Ping(req dto.NetToolReq) (string, error) { + if err := validateHost(req.Host); err != nil { + return "", err + } + count := "4" + if req.Count > 0 && req.Count <= 20 { + count = fmt.Sprintf("%d", req.Count) + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + out, err := exec.CommandContext(ctx, "ping", "-c", count, "-W", "5", req.Host).CombinedOutput() + if err != nil && len(out) == 0 { + return "", fmt.Errorf("ping failed: %v", err) + } + return string(out), nil +} + +func (t *ToolboxNetService) Traceroute(req dto.NetToolReq) (string, error) { + if err := validateHost(req.Host); err != nil { + return "", err + } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + // Try traceroute first, fall back to tracepath + binary := "traceroute" + if !cmd.Which("traceroute") { + binary = "tracepath" + if !cmd.Which("tracepath") { + return "", fmt.Errorf("neither traceroute nor tracepath is installed") + } + } + + var args []string + if binary == "traceroute" { + args = []string{"-m", "20", "-w", "3", req.Host} + } else { + args = []string{"-m", "20", req.Host} + } + + out, err := exec.CommandContext(ctx, binary, args...).CombinedOutput() + if err != nil && len(out) == 0 { + return "", fmt.Errorf("%s failed: %v", binary, err) + } + return string(out), nil +} + +func (t *ToolboxNetService) DNSLookup(req dto.NetToolReq) (string, error) { + if err := validateHost(req.Host); err != nil { + return "", err + } + var result strings.Builder + + ips, err := net.LookupHost(req.Host) + if err != nil { + return "", fmt.Errorf("DNS lookup failed: %v", err) + } + result.WriteString(fmt.Sprintf("Host: %s\n\n", req.Host)) + result.WriteString("IP Addresses:\n") + for _, ip := range ips { + result.WriteString(fmt.Sprintf(" %s\n", ip)) + } + + cname, err := net.LookupCNAME(req.Host) + if err == nil && cname != req.Host+"." { + result.WriteString(fmt.Sprintf("\nCNAME: %s\n", cname)) + } + + mxs, err := net.LookupMX(req.Host) + if err == nil && len(mxs) > 0 { + result.WriteString("\nMX Records:\n") + for _, mx := range mxs { + result.WriteString(fmt.Sprintf(" %s (priority: %d)\n", mx.Host, mx.Pref)) + } + } + + nss, err := net.LookupNS(req.Host) + if err == nil && len(nss) > 0 { + result.WriteString("\nNS Records:\n") + for _, ns := range nss { + result.WriteString(fmt.Sprintf(" %s\n", ns.Host)) + } + } + + txts, err := net.LookupTXT(req.Host) + if err == nil && len(txts) > 0 { + result.WriteString("\nTXT Records:\n") + for _, txt := range txts { + result.WriteString(fmt.Sprintf(" %s\n", txt)) + } + } + + return result.String(), nil +} + +func (t *ToolboxNetService) HTTPCheck(req dto.NetToolReq) (string, error) { + url := req.Host + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + url = "http://" + url + } + + client := &http.Client{ + Timeout: 15 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) >= 10 { + return fmt.Errorf("too many redirects") + } + return nil + }, + } + + start := time.Now() + resp, err := client.Get(url) + elapsed := time.Since(start) + if err != nil { + return "", fmt.Errorf("HTTP request failed: %v", err) + } + defer resp.Body.Close() + + var result strings.Builder + result.WriteString(fmt.Sprintf("URL: %s\n", url)) + result.WriteString(fmt.Sprintf("Status: %s\n", resp.Status)) + result.WriteString(fmt.Sprintf("Response Time: %dms\n", elapsed.Milliseconds())) + result.WriteString(fmt.Sprintf("Protocol: %s\n", resp.Proto)) + result.WriteString(fmt.Sprintf("\nHeaders:\n")) + for key, values := range resp.Header { + for _, value := range values { + result.WriteString(fmt.Sprintf(" %s: %s\n", key, value)) + } + } + + return result.String(), nil +} + +func validateHost(host string) error { + if host == "" { + return fmt.Errorf("host is required") + } + if strings.ContainsAny(host, ";|&$`(){}[]!><\n\r") { + return fmt.Errorf("invalid host: contains illegal characters") + } + if len(host) > 253 { + return fmt.Errorf("host too long") + } + return nil +} diff --git a/agent/router/ro_toolbox.go b/agent/router/ro_toolbox.go index 06693e8e73d0..b6c98d68a3d4 100644 --- a/agent/router/ro_toolbox.go +++ b/agent/router/ro_toolbox.go @@ -54,5 +54,7 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) { toolboxRouter.POST("/clam/status/update", baseApi.UpdateClamStatus) toolboxRouter.POST("/clam/del", baseApi.DeleteClam) toolboxRouter.POST("/clam/handle", baseApi.HandleClamScan) + + toolboxRouter.POST("/net", baseApi.RunNetTool) } } diff --git a/frontend/src/api/modules/toolbox-net.ts b/frontend/src/api/modules/toolbox-net.ts new file mode 100644 index 000000000000..a5bc919f4a5d --- /dev/null +++ b/frontend/src/api/modules/toolbox-net.ts @@ -0,0 +1,16 @@ +import http from '@/api'; +import { TimeoutEnum } from '@/enums/http-enum'; + +export interface NetToolReq { + tool: 'ping' | 'traceroute' | 'dns' | 'http'; + host: string; + count?: number; +} + +export interface NetToolRes { + output: string; +} + +export const runNetTool = (req: NetToolReq) => { + return http.post('/toolbox/net', req, TimeoutEnum.T_60S); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 1b235bb83d83..b50571c2b501 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1501,6 +1501,18 @@ const message = { dirHelper: 'Enabling FTP requires directory permission changes - please choose carefully', dirMsg: 'Enabling FTP will modify permissions for the entire {0} directory. Continue?', }, + net: { + title: 'Network Tools', + tool: 'Tool', + host: 'Host / URL', + run: 'Run', + result: 'Result', + waiting: 'Running...', + failed: 'Request failed', + pingCount: 'packets', + dnsLookup: 'DNS Lookup', + httpCheck: 'HTTP Check', + }, clam: { clam: 'Virus scan', cron: 'Scheduled scan', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 1125086779d7..c5903918208e 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1415,6 +1415,18 @@ const message = { dirHelper: '开启 FTP 需要修改目录权限,请谨慎选择', dirMsg: '开启 FTP 将修改整个 {0} 目录权限,是否继续?', }, + net: { + title: '网络工具', + tool: '工具', + host: '主机 / URL', + run: '执行', + result: '结果', + waiting: '执行中...', + failed: '请求失败', + pingCount: '个数据包', + dnsLookup: 'DNS 查询', + httpCheck: 'HTTP 检测', + }, clam: { clam: '病毒扫描', cron: '定时扫描', diff --git a/frontend/src/routers/modules/toolbox.ts b/frontend/src/routers/modules/toolbox.ts index 06a047e9a329..c68710ea28a9 100644 --- a/frontend/src/routers/modules/toolbox.ts +++ b/frontend/src/routers/modules/toolbox.ts @@ -89,6 +89,18 @@ const toolboxRouter = { requiresAuth: false, }, }, + { + path: 'net', + name: 'NetTools', + component: () => import('@/views/toolbox/net/index.vue'), + hidden: true, + meta: { + parent: 'menu.toolbox', + title: 'toolbox.net.title', + activeMenu: '/toolbox', + requiresAuth: false, + }, + }, { path: 'clean', name: 'Clean', diff --git a/frontend/src/views/toolbox/index.vue b/frontend/src/views/toolbox/index.vue index d3d3fd210aba..a5be2b7af98f 100644 --- a/frontend/src/views/toolbox/index.vue +++ b/frontend/src/views/toolbox/index.vue @@ -77,5 +77,9 @@ const buttons = [ label: 'Fail2ban', path: '/toolbox/fail2ban', }, + { + label: i18n.global.t('toolbox.net.title'), + path: '/toolbox/net', + }, ]; diff --git a/frontend/src/views/toolbox/net/index.vue b/frontend/src/views/toolbox/net/index.vue new file mode 100644 index 000000000000..a9fc0103fc75 --- /dev/null +++ b/frontend/src/views/toolbox/net/index.vue @@ -0,0 +1,103 @@ + + + + +