271 lines
7.5 KiB
Go
271 lines
7.5 KiB
Go
|
// Licensed to Elasticsearch B.V. under one or more contributor
|
||
|
// license agreements. See the NOTICE file distributed with
|
||
|
// this work for additional information regarding copyright
|
||
|
// ownership. Elasticsearch B.V. licenses this file to you under
|
||
|
// the Apache License, Version 2.0 (the "License"); you may
|
||
|
// not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing,
|
||
|
// software distributed under the License is distributed on an
|
||
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||
|
// KIND, either express or implied. See the License for the
|
||
|
// specific language governing permissions and limitations
|
||
|
// under the License.
|
||
|
|
||
|
package procs
|
||
|
|
||
|
import (
|
||
|
"net"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/elastic/beats/libbeat/common"
|
||
|
"github.com/elastic/beats/libbeat/logp"
|
||
|
"github.com/elastic/beats/packetbeat/protos/applayer"
|
||
|
"github.com/elastic/gosigar"
|
||
|
)
|
||
|
|
||
|
// This controls how often process info for a running process is reloaded
|
||
|
// A big value means less unnecessary refreshes at a higher risk of missing
|
||
|
// a PID being recycled by the OS
|
||
|
const processCacheExpiration = time.Second * 30
|
||
|
|
||
|
type portProcMapping struct {
|
||
|
port uint16
|
||
|
pid int
|
||
|
proc *process
|
||
|
}
|
||
|
|
||
|
type process struct {
|
||
|
name string
|
||
|
commandLine string
|
||
|
// To control cache expiration
|
||
|
expiration time.Time
|
||
|
}
|
||
|
|
||
|
// Allow the OS-dependant implementation to be replaced by a mock for testing
|
||
|
type processWatcherImpl interface {
|
||
|
// GetLocalPortToPIDMapping returns the list of local port numbers and the PID
|
||
|
// that owns them.
|
||
|
GetLocalPortToPIDMapping(transport applayer.Transport) (ports map[uint16]int, err error)
|
||
|
// GetProcessCommandLine returns the command line for a given process.
|
||
|
GetProcessCommandLine(pid int) string
|
||
|
// GetLocalIPs returns the list of local addresses.
|
||
|
GetLocalIPs() ([]net.IP, error)
|
||
|
}
|
||
|
|
||
|
type ProcessesWatcher struct {
|
||
|
portProcMap map[applayer.Transport]map[uint16]portProcMapping
|
||
|
localAddrs []net.IP
|
||
|
processCache map[int]*process
|
||
|
|
||
|
// config
|
||
|
enabled bool
|
||
|
procConfig []ProcConfig
|
||
|
|
||
|
impl processWatcherImpl
|
||
|
}
|
||
|
|
||
|
var ProcWatcher ProcessesWatcher
|
||
|
|
||
|
func (proc *ProcessesWatcher) Init(config ProcsConfig) error {
|
||
|
return proc.initWithImpl(config, proc)
|
||
|
}
|
||
|
|
||
|
func (proc *ProcessesWatcher) initWithImpl(config ProcsConfig, impl processWatcherImpl) error {
|
||
|
proc.impl = impl
|
||
|
proc.portProcMap = map[applayer.Transport]map[uint16]portProcMapping{
|
||
|
applayer.TransportUDP: make(map[uint16]portProcMapping),
|
||
|
applayer.TransportTCP: make(map[uint16]portProcMapping),
|
||
|
}
|
||
|
|
||
|
proc.processCache = make(map[int]*process)
|
||
|
|
||
|
proc.enabled = config.Enabled
|
||
|
if proc.enabled {
|
||
|
logp.Info("Process watcher enabled")
|
||
|
} else {
|
||
|
logp.Info("Process watcher disabled")
|
||
|
}
|
||
|
|
||
|
// Read the local IP addresses
|
||
|
var err error
|
||
|
proc.localAddrs, err = impl.GetLocalIPs()
|
||
|
if err != nil {
|
||
|
logp.Err("Error getting local IP addresses: %s", err)
|
||
|
proc.localAddrs = []net.IP{}
|
||
|
}
|
||
|
|
||
|
proc.procConfig = config.Monitored
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// FindProcessesTupleTCP looks up local process information for the source and
|
||
|
// destination addresses of TCP tuple
|
||
|
func (proc *ProcessesWatcher) FindProcessesTupleTCP(tuple *common.IPPortTuple) (procTuple *common.CmdlineTuple) {
|
||
|
return proc.FindProcessesTuple(tuple, applayer.TransportTCP)
|
||
|
}
|
||
|
|
||
|
// FindProcessesTupleUDP looks up local process information for the source and
|
||
|
// destination addresses of UDP tuple
|
||
|
func (proc *ProcessesWatcher) FindProcessesTupleUDP(tuple *common.IPPortTuple) (procTuple *common.CmdlineTuple) {
|
||
|
return proc.FindProcessesTuple(tuple, applayer.TransportUDP)
|
||
|
}
|
||
|
|
||
|
// FindProcessesTuple looks up local process information for the source and
|
||
|
// destination addresses of a tuple for the given transport protocol
|
||
|
func (proc *ProcessesWatcher) FindProcessesTuple(tuple *common.IPPortTuple, transport applayer.Transport) (procTuple *common.CmdlineTuple) {
|
||
|
procTuple = &common.CmdlineTuple{}
|
||
|
|
||
|
if !proc.enabled {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if proc.isLocalIP(tuple.SrcIP) {
|
||
|
if p := proc.findProc(tuple.SrcPort, transport); p != nil {
|
||
|
procTuple.Src = []byte(p.name)
|
||
|
procTuple.SrcCommand = []byte(p.commandLine)
|
||
|
logp.Debug("procs", "Found process '%s' (%s) for port %d/%s", p.commandLine, p.name, tuple.SrcPort, transport)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if proc.isLocalIP(tuple.DstIP) {
|
||
|
if p := proc.findProc(tuple.DstPort, transport); p != nil {
|
||
|
procTuple.Dst = []byte(p.name)
|
||
|
procTuple.DstCommand = []byte(p.commandLine)
|
||
|
logp.Debug("procs", "Found process '%s' (%s) for port %d/%s", p.commandLine, p.name, tuple.DstPort, transport)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (proc *ProcessesWatcher) findProc(port uint16, transport applayer.Transport) *process {
|
||
|
defer logp.Recover("FindProc exception")
|
||
|
|
||
|
procMap, ok := proc.portProcMap[transport]
|
||
|
if !ok {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
p, exists := procMap[port]
|
||
|
if exists {
|
||
|
return p.proc
|
||
|
}
|
||
|
|
||
|
proc.updateMap(transport)
|
||
|
|
||
|
p, exists = procMap[port]
|
||
|
if exists {
|
||
|
return p.proc
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (proc *ProcessesWatcher) updateMap(transport applayer.Transport) {
|
||
|
if logp.HasSelector("procsdetailed") {
|
||
|
start := time.Now()
|
||
|
defer func() {
|
||
|
logp.Debug("procsdetailed", "updateMap() took %v", time.Now().Sub(start))
|
||
|
}()
|
||
|
}
|
||
|
|
||
|
ports, err := proc.impl.GetLocalPortToPIDMapping(transport)
|
||
|
if err != nil {
|
||
|
logp.Err("unable to list local ports: %v", err)
|
||
|
}
|
||
|
|
||
|
proc.expireProcessCache()
|
||
|
|
||
|
for port, pid := range ports {
|
||
|
proc.updateMappingEntry(transport, port, pid)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (proc *ProcessesWatcher) updateMappingEntry(transport applayer.Transport, port uint16, pid int) {
|
||
|
prev, ok := proc.portProcMap[transport][port]
|
||
|
if ok && prev.pid == pid {
|
||
|
// This port->pid mapping already exists
|
||
|
return
|
||
|
}
|
||
|
|
||
|
p := proc.getProcessInfo(pid)
|
||
|
if p == nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Simply overwrite old entries for now.
|
||
|
// We never expire entries from this map. Since there are 65k possible
|
||
|
// ports, the size of the dict can be max 1.5 MB, which we consider
|
||
|
// reasonable.
|
||
|
proc.portProcMap[transport][port] = portProcMapping{port: port, pid: pid, proc: p}
|
||
|
|
||
|
logp.Debug("procsdetailed", "updateMappingEntry(): port=%d/%s pid=%d process='%s' name=%s",
|
||
|
port, transport, pid, p.commandLine, p.name)
|
||
|
}
|
||
|
|
||
|
func (proc *ProcessesWatcher) isLocalIP(ip net.IP) bool {
|
||
|
if ip.IsLoopback() {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
for _, addr := range proc.localAddrs {
|
||
|
if ip.Equal(addr) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (proc *ProcessesWatcher) getProcessInfo(pid int) *process {
|
||
|
if p, ok := proc.processCache[pid]; ok {
|
||
|
return p
|
||
|
}
|
||
|
// Not in cache, resolve process info
|
||
|
p := &process{
|
||
|
commandLine: proc.impl.GetProcessCommandLine(pid),
|
||
|
expiration: time.Now().Add(processCacheExpiration),
|
||
|
}
|
||
|
// see if the command-line matches any 'grep' pattern
|
||
|
for _, match := range proc.procConfig {
|
||
|
if strings.Contains(p.commandLine, match.CmdlineGrep) {
|
||
|
p.name = match.Process
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
proc.processCache[pid] = p
|
||
|
return p
|
||
|
}
|
||
|
|
||
|
func (proc *ProcessesWatcher) expireProcessCache() {
|
||
|
now := time.Now()
|
||
|
for pid, info := range proc.processCache {
|
||
|
if now.After(info.expiration) {
|
||
|
delete(proc.processCache, pid)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// GetProcessCommandLine returns the command line for a given process.
|
||
|
func (proc *ProcessesWatcher) GetProcessCommandLine(pid int) (cmdLine string) {
|
||
|
var procArgs gosigar.ProcArgs
|
||
|
if err := procArgs.Get(pid); err == nil {
|
||
|
cmdLine = strings.Join(procArgs.List, " ")
|
||
|
} else {
|
||
|
// Save PID without command-line to avoid continued errors for this process
|
||
|
logp.Err("Unable to get command-line for pid %d: %v", pid, err)
|
||
|
}
|
||
|
return cmdLine
|
||
|
}
|
||
|
|
||
|
// GetLocalIPs returns the list of local addresses.
|
||
|
func (proc *ProcessesWatcher) GetLocalIPs() ([]net.IP, error) {
|
||
|
return common.LocalIPAddrs()
|
||
|
}
|