Files
aiops/agent/collector/port.go
T
Logan.Li e0d652d211 feat: 完善堡垒机与 Agent 智能运维能力
- 支持 Windows Agent 构建、安装脚本和统一上报协议

- 增强 RDP 剪贴板、虚拟盘和文件传输能力

- 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口

- 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
2026-07-05 23:49:36 +08:00

274 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现监听 TCP 端口的自动发现:
// - 扫描 /proc/net/tcp 与 /proc/net/tcp6 中状态为 LISTEN(0A) 的套接字;
// - 通过遍历 /proc/*/fd 反查套接字 inode 归属的进程。
// - 内置系统进程排除列表(sshd、rpcbind、master 等永远不上报)。
// - 支持远程端口策略:whitelist(全量上报,默认)/ blacklist(只上报白名单端口)。
//
// 署名 Logan.Li
package collector
import (
"os"
"path/filepath"
"strconv"
"strings"
"ezops/agent/model"
)
// 系统级进程 — 永远不采集其监听端口
var systemProcesses = map[string]bool{
"sshd": true,
"rpcbind": true,
"master": true, // Postfix
"systemd": true,
"systemd-resolve": true,
"systemd-journal": true,
"dbus-daemon": true,
"rsyslogd": true,
"irqbalance": true,
"polkitd": true,
"accounts-daemon": true,
"NetworkManager": true,
"udisksd": true,
"upowerd": true,
"rtkit-daemon": true,
"switcheroo-contr": true,
"power-profiles-": true, // power-profiles-daemon
"wpa_supplicant": true,
"avahi-daemon": true,
"cron": true,
"atd": true,
"agetty": true,
}
// PortCollector 监听端口自动发现采集器
type PortCollector struct {
Policy *model.PortPolicy // 可选:后端下发的端口过滤策略
}
// NewPortCollector 创建端口采集器实例
func NewPortCollector() *PortCollector {
return &PortCollector{}
}
// SetPolicy 设置端口过滤策略(daemon 模式下从后端轮询获得)
func (p *PortCollector) SetPolicy(policy *model.PortPolicy) {
p.Policy = policy
}
// listenSocket 描述一个处于 LISTEN 状态的套接字
type listenSocket struct {
port int
protocol string
inode uint64
}
// procInfo 描述占用某套接字 inode 的进程信息
type procInfo struct {
pid int
name string
}
// Collect 采集所有处于监听状态的 TCP 端口及其归属进程
func (p *PortCollector) Collect() ([]model.PortStat, error) {
// 1. 扫描 /proc/net/tcp(/tcp6),收集 LISTEN 套接字
sockets := p.scanListenSockets()
if len(sockets) == 0 {
return []model.PortStat{}, nil
}
// 2. 构建 inode -> 进程信息 映射(仅 Linux 上可用)
inodeMap := p.buildInodeMap()
// 3. 组装结果并按 (端口, 协议, PID) 去重,应用过滤策略
seen := make(map[string]bool)
ports := make([]model.PortStat, 0, len(sockets))
for _, s := range sockets {
pid, name := 0, ""
if info, ok := inodeMap[s.inode]; ok {
pid, name = info.pid, info.name
}
// ── 过滤逻辑 ──
// 1) 永远排除系统级进程端口
if isSystemProcess(name) {
continue
}
// 2) 黑名单模式:只上报在白名单中的端口
if p.Policy != nil && p.Policy.IsBlacklistMode() {
if !p.Policy.InWhitelist(s.port) {
continue
}
}
// 白名单模式(默认):全量上报已排除系统进程的端口
// ── 过滤逻辑结束 ──
key := strconv.Itoa(s.port) + "|" + s.protocol + "|" + strconv.Itoa(pid)
if seen[key] {
continue
}
seen[key] = true
ports = append(ports, model.PortStat{
Port: s.port,
Protocol: s.protocol,
Process: name,
PID: pid,
})
}
return ports, nil
}
// scanListenSockets 解析 /proc/net/tcp 与 /proc/net/tcp6,返回 LISTEN 套接字。
// 单个文件读取失败(如 macOS 开发环境下不存在 /proc)会被忽略。
func (p *PortCollector) scanListenSockets() []listenSocket {
files := []struct {
path string
protocol string
}{
{"/proc/net/tcp", "tcp"},
{"/proc/net/tcp6", "tcp6"},
}
var sockets []listenSocket
for _, sf := range files {
result, err := parseNetTCP(sf.path, sf.protocol)
if err != nil {
continue
}
sockets = append(sockets, result...)
}
return sockets
}
// parseNetTCP 解析单个 /proc/net/tcp(*) 文件
func parseNetTCP(path, protocol string) ([]listenSocket, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var sockets []listenSocket
lines := strings.Split(string(data), "\n")
for i, line := range lines {
if i == 0 {
continue // 跳过表头
}
fields := strings.Fields(line)
if len(fields) < 10 {
continue
}
// fields[3] 为套接字状态,"0A" 表示 TCP_LISTEN
if fields[3] != "0A" {
continue
}
// fields[1] 为本地地址,格式 IP:Port(均为十六进制)
localAddr := fields[1]
colon := strings.LastIndex(localAddr, ":")
if colon < 0 {
continue
}
port, err := strconv.ParseUint(localAddr[colon+1:], 16, 32)
if err != nil {
continue
}
// fields[9] 为套接字 inode
inode, err := strconv.ParseUint(fields[9], 10, 64)
if err != nil {
continue
}
sockets = append(sockets, listenSocket{
port: int(port),
protocol: protocol,
inode: inode,
})
}
return sockets, nil
}
// buildInodeMap 扫描 /proc/*/fd,构建 套接字 inode -> 进程信息 的映射。
// 对无权限读取的进程目录(如其它用户进程)会自动跳过。
func (p *PortCollector) buildInodeMap() map[uint64]procInfo {
inodeMap := make(map[uint64]procInfo)
entries, err := os.ReadDir("/proc")
if err != nil {
return inodeMap
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
pid, err := strconv.Atoi(entry.Name())
if err != nil {
continue // 非 PID 目录
}
fdDir := filepath.Join("/proc", entry.Name(), "fd")
fds, err := os.ReadDir(fdDir)
if err != nil {
continue // 无权限读取,跳过
}
name := readProcessName(entry.Name())
for _, fd := range fds {
link, err := os.Readlink(filepath.Join(fdDir, fd.Name()))
if err != nil {
continue
}
// 套接字链接形如: socket:[12345]
if !strings.HasPrefix(link, "socket:[") {
continue
}
inodeStr := link[len("socket:[") : len(link)-1]
inode, err := strconv.ParseUint(inodeStr, 10, 64)
if err != nil {
continue
}
// 同一 inode 仅记录首个发现的进程即可
if _, exists := inodeMap[inode]; !exists {
inodeMap[inode] = procInfo{pid: pid, name: name}
}
}
}
return inodeMap
}
// readProcessName 读取进程名称:优先 /proc/<pid>/comm,失败则尝试取 cmdline 的可执行文件名
func readProcessName(pid string) string {
if data, err := os.ReadFile("/proc/" + pid + "/comm"); err == nil {
if s := strings.TrimSpace(string(data)); s != "" {
return s
}
}
if data, err := os.ReadFile("/proc/" + pid + "/cmdline"); err == nil {
// cmdline 以 \0 分隔参数,取首个(即可执行文件路径)的基名
parts := strings.SplitN(string(data), "\x00", 2)
if len(parts) > 0 && parts[0] != "" {
return filepath.Base(parts[0])
}
}
return ""
}
// isSystemProcess 判断进程名是否为系统进程(永远不上报的端口)
func isSystemProcess(name string) bool {
if name == "" {
return false
}
// 精确匹配
if systemProcesses[name] {
return true
}
// 前缀匹配(如 power-profiles-daemon)
for prefix := range systemProcesses {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}