feat: retrieve listening processes in firewall port page (#11665)

* feat: Add endpoint to retrieve listening processes and update related services

* fix: Update listening process retrieval logic to include socket type checks

* refactor:  accept context for improved request handling
This commit is contained in:
KOMATA
2026-01-16 16:54:15 +08:00
committed by GitHub
parent 365936bf57
commit e5b7e99cce
7 changed files with 155 additions and 37 deletions
+15
View File
@@ -57,3 +57,18 @@ func (b *BaseApi) GetProcessInfoByPID(c *gin.Context) {
}
helper.SuccessWithData(c, data)
}
// @Tags Process
// @Summary Get Listening Process
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /process/listening [post]
func (b *BaseApi) GetListeningProcess(c *gin.Context) {
procs, err := processService.GetListeningProcess(c)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, procs)
}
+2 -9
View File
@@ -720,16 +720,9 @@ func checkPortUsed(ports, proto string, apps []portOfApp) string {
for _, app := range apps {
if app.HttpPort == ports || app.HttpsPort == ports {
return fmt.Sprintf("(%s)", app.AppName)
return app.AppName
}
}
port, err := strconv.Atoi(ports)
if err != nil {
global.LOG.Errorf(" convert string %v to int failed, err: %v", port, err)
return ""
}
if common.ScanPortWithProto(port, proto) {
return "inUsed"
}
return ""
}
+58 -4
View File
@@ -2,15 +2,19 @@ package service
import (
"bufio"
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/websocket"
"github.com/shirou/gopsutil/v4/process"
"os"
"strconv"
"strings"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/websocket"
"github.com/shirou/gopsutil/v4/net"
"github.com/shirou/gopsutil/v4/process"
)
type ProcessService struct{}
@@ -18,6 +22,7 @@ type ProcessService struct{}
type IProcessService interface {
StopProcess(req request.ProcessReq) error
GetProcessInfoByPID(pid int32) (*websocket.PsProcessData, error)
GetListeningProcess(c context.Context) ([]ListeningProcess, error)
}
func NewIProcessService() IProcessService {
@@ -35,6 +40,55 @@ func (ps *ProcessService) StopProcess(req request.ProcessReq) error {
return nil
}
type ListeningProcess struct {
PID int32
Port map[uint32]struct{}
Protocol uint32
Name string
}
func (ps *ProcessService) GetListeningProcess(c context.Context) ([]ListeningProcess, error) {
conn, err := net.ConnectionsMaxWithContext(c, "inet", 32768)
if err != nil {
return nil, err
}
procCache := make(map[int32]ListeningProcess, 64)
for _, conn := range conn {
if conn.Pid == 0 {
continue
}
if (conn.Status == "LISTEN" && conn.Type == syscall.SOCK_STREAM) || (conn.Type == syscall.SOCK_DGRAM && conn.Raddr.Port == 0) {
if _, exists := procCache[conn.Pid]; !exists {
proc, err := process.NewProcess(conn.Pid)
if err != nil {
continue
}
procData := ListeningProcess{
PID: conn.Pid,
}
procData.Name, _ = proc.Name()
procData.Port = make(map[uint32]struct{})
procData.Port[conn.Laddr.Port] = struct{}{}
procData.Protocol = conn.Type
procCache[conn.Pid] = procData
} else {
p := procCache[conn.Pid]
p.Port[conn.Laddr.Port] = struct{}{}
procCache[conn.Pid] = p
}
}
}
procs := make([]ListeningProcess, 0, len(procCache))
for _, proc := range procCache {
procs = append(procs, proc)
}
return procs, nil
}
func (ps *ProcessService) GetProcessInfoByPID(pid int32) (*websocket.PsProcessData, error) {
p, err := process.NewProcess(pid)
if err != nil {
+1
View File
@@ -14,6 +14,7 @@ func (f *ProcessRouter) InitRouter(Router *gin.RouterGroup) {
{
processRouter.GET("/ws", baseApi.ProcessWs)
processRouter.POST("/stop", baseApi.StopProcess)
processRouter.POST("/listening", baseApi.GetListeningProcess)
processRouter.GET("/:pid", baseApi.GetProcessInfoByPID)
}
}
+7
View File
@@ -55,4 +55,11 @@ export namespace Process {
path: string;
fd: number;
}
export interface ListeningProcess {
PID: number;
Port: { [key: string]: {} };
Protocol: number;
Name: string;
}
}
+4
View File
@@ -8,3 +8,7 @@ export const stopProcess = (req: Process.StopReq) => {
export const getProcessByID = (pid: number) => {
return http.get<Process.PsProcessData>(`/process/${pid}`);
};
export const getListeningProcess = () => {
return http.post<Process.ListeningProcess[]>(`/process/listening`);
};
+68 -24
View File
@@ -88,28 +88,20 @@
<el-table-column :label="$t('commons.table.port')" :min-width="70" prop="port" />
<el-table-column :label="$t('commons.table.status')" :min-width="120">
<template #default="{ row }">
<div v-if="row.port.indexOf('-') !== -1 && row.usedStatus">
<el-tag type="info" class="mt-1">
{{ $t('firewall.used') + ' * ' + row.usedPorts.length }}
<div v-if="isSinglePort(row.port)">
<el-tag type="success" v-if="row.usedStatus">
{{ $t('firewall.used') + ' (' + row.usedStatus + ')' }}
<el-icon
v-if="row.processInfo"
@click="showProcessDetail(row.processInfo.PID)"
style="margin-left: 4px; cursor: pointer; vertical-align: middle"
>
<Expand />
</el-icon>
</el-tag>
<el-popover placement="right" popper-class="limit-height-popover" :width="250">
<template #default>
<ul v-for="(item, index) in row.usedPorts" :key="index">
<li>{{ item }}</li>
</ul>
</template>
<template #reference>
<svg-icon iconName="p-xiangqing" class="svg-icon"></svg-icon>
</template>
</el-popover>
</div>
<div v-else>
<el-tag type="info" v-if="row.usedStatus">
<span v-if="row.usedStatus === 'inUsed'">{{ $t('firewall.used') }}</span>
<span v-else>{{ $t('firewall.used') + ' ' + row.usedStatus }}</span>
</el-tag>
<el-tag type="success" v-else>{{ $t('firewall.unUsed') }}</el-tag>
<el-tag type="info" v-else>{{ $t('firewall.unUsed') }}</el-tag>
</div>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column :min-width="80" :label="$t('firewall.strategy')" prop="strategy">
@@ -163,6 +155,7 @@
<OpDialog ref="opRef" @search="search" />
<OperateDialog @search="search" ref="dialogRef" />
<ImportDialog @search="search" ref="dialogImportRef" />
<ProcessDetail ref="processDetailRef" />
</div>
</template>
@@ -171,12 +164,16 @@ import FireRouter from '@/views/host/firewall/index.vue';
import OperateDialog from '@/views/host/firewall/port/operate/index.vue';
import ImportDialog from '@/views/host/firewall/port/import/index.vue';
import FireStatus from '@/views/host/firewall/status/index.vue';
import ProcessDetail from '@/views/host/process/process/detail/index.vue';
import { onMounted, reactive, ref } from 'vue';
import { batchOperateRule, searchFireRule, updateFirewallDescription, updatePortRule } from '@/api/modules/host';
import { getListeningProcess } from '@/api/modules/process';
import { Host } from '@/api/interface/host';
import { Process } from '@/api/interface/process';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { ElMessageBox } from 'element-plus';
import { Expand } from '@element-plus/icons-vue';
import { routerToName } from '@/utils/router';
import { downloadWithContent, getCurrentDateFormatted } from '@/utils/util';
@@ -195,6 +192,9 @@ const fireStatusRef = ref();
const opRef = ref();
const dialogImportRef = ref();
const processDetailRef = ref();
const listeningProcesses = ref<Process.ListeningProcess[]>([]);
const data = ref();
const paginationConfig = reactive({
@@ -204,6 +204,46 @@ const paginationConfig = reactive({
total: 0,
});
const extractPortsFromObject = (portObj: { [key: string]: {} }): number[] => {
return Object.keys(portObj)
.map((portStr) => parseInt(portStr))
.filter((port) => !isNaN(port));
};
const isSinglePort = (portStr: string): boolean => {
return portStr.indexOf('-') === -1 && portStr.indexOf(':') === -1 && portStr.indexOf(',') === -1;
};
const loadListeningProcesses = async () => {
try {
const res = await getListeningProcess();
listeningProcesses.value = res.data || [];
for (const item of data.value) {
if (!item.usedStatus && isSinglePort(item.port)) {
const portNum = parseInt(item.port.trim());
if (!isNaN(portNum)) {
const protocolNum =
item.protocol.toLowerCase() === 'tcp' ? 1 : item.protocol.toLowerCase() === 'udp' ? 2 : 0;
for (const proc of listeningProcesses.value) {
if (proc.Protocol === protocolNum) {
const procPorts = extractPortsFromObject(proc.Port);
if (procPorts.includes(portNum)) {
item.usedStatus = proc.Name;
item.processInfo = proc;
break;
}
}
}
}
}
}
} catch (error) {
console.error('Failed to load listening processes:', error);
}
};
const search = async () => {
if (!isActive.value) {
loading.value = false;
@@ -221,12 +261,12 @@ const search = async () => {
};
loading.value = true;
await searchFireRule(params)
.then((res) => {
.then(async (res) => {
loading.value = false;
data.value = res.data.items || [];
for (const item of data.value) {
item.usedPorts = item.usedStatus ? item.usedStatus.split(',') : [];
}
await loadListeningProcesses();
paginationConfig.total = res.data.total;
})
.catch(() => {
@@ -381,6 +421,10 @@ const onExport = () => {
});
};
const showProcessDetail = (pid: number) => {
processDetailRef.value?.acceptParams(pid);
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),