- 支持 Windows Agent 构建、安装脚本和统一上报协议 - 增强 RDP 剪贴板、虚拟盘和文件传输能力 - 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口 - 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
274 lines
7.1 KiB
Go
274 lines
7.1 KiB
Go
//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
|
||
}
|