Skip to content
Open
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
3 changes: 2 additions & 1 deletion agent/app/api/v2/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,6 @@ var (
groupService = service.NewIGroupService()
alertService = service.NewIAlertService()

diskService = service.NewIDiskService()
diskService = service.NewIDiskService()
toolboxNetService = service.NewIToolboxNetService()
)
42 changes: 42 additions & 0 deletions agent/app/api/v2/toolbox_net.go
Original file line number Diff line number Diff line change
@@ -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})
}
11 changes: 11 additions & 0 deletions agent/app/dto/toolbox.go
Original file line number Diff line number Diff line change
@@ -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"`
}
174 changes: 174 additions & 0 deletions agent/app/service/toolbox_net.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions agent/router/ro_toolbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
16 changes: 16 additions & 0 deletions frontend/src/api/modules/toolbox-net.ts
Original file line number Diff line number Diff line change
@@ -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<NetToolRes>('/toolbox/net', req, TimeoutEnum.T_60S);
};
12 changes: 12 additions & 0 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/lang/modules/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '定时扫描',
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/routers/modules/toolbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/views/toolbox/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,9 @@ const buttons = [
label: 'Fail2ban',
path: '/toolbox/fail2ban',
},
{
label: i18n.global.t('toolbox.net.title'),
path: '/toolbox/net',
},
];
</script>
Loading