youtubebeat/vendor/github.com/elastic/beats/metricbeat/module/system/socket/socket.go

319 lines
8.7 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.
// +build linux
package socket
import (
"fmt"
"net"
"os"
"path/filepath"
"sync/atomic"
"syscall"
"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/libbeat/logp"
"github.com/elastic/beats/metricbeat/mb"
"github.com/elastic/beats/metricbeat/mb/parse"
"github.com/elastic/beats/metricbeat/module/system"
"github.com/elastic/gosigar/sys/linux"
"github.com/pkg/errors"
)
var (
debugSelector = "system.socket"
debugf = logp.MakeDebug(debugSelector)
)
func init() {
mb.Registry.MustAddMetricSet("system", "socket", New,
mb.WithHostParser(parse.EmptyHostParser),
)
}
type MetricSet struct {
mb.BaseMetricSet
readBuffer []byte
seq uint32
ptable *ProcTable
euid int
previousConns hashSet
currentConns hashSet
reverseLookup *ReverseLookupCache
listeners *ListenerTable
users UserCache
}
func New(base mb.BaseMetricSet) (mb.MetricSet, error) {
c := defaultConfig
if err := base.Module().UnpackConfig(&c); err != nil {
return nil, err
}
systemModule, ok := base.Module().(*system.Module)
if !ok {
return nil, errors.New("unexpected module type")
}
ptable, err := NewProcTable(filepath.Join(systemModule.HostFS, "/proc"))
if err != nil {
return nil, err
}
if os.Geteuid() != 0 {
logp.Info("socket process info will only be available for " +
"metricbeat because the process is running as a non-root user")
}
m := &MetricSet{
BaseMetricSet: base,
readBuffer: make([]byte, os.Getpagesize()),
ptable: ptable,
euid: os.Geteuid(),
previousConns: hashSet{},
currentConns: hashSet{},
listeners: NewListenerTable(),
users: NewUserCache(),
}
if c.ReverseLookup.IsEnabled() {
var successTTL, failureTTL = defSuccessTTL, defFailureTTL
if c.ReverseLookup.SuccessTTL != 0 {
successTTL = c.ReverseLookup.SuccessTTL
}
if c.ReverseLookup.FailureTTL != 0 {
successTTL = c.ReverseLookup.FailureTTL
}
debugf("enabled reverse DNS lookup with cache TTL of %v/%v",
successTTL, failureTTL)
m.reverseLookup = NewReverseLookupCache(successTTL, failureTTL)
}
return m, nil
}
func (m *MetricSet) Fetch() ([]common.MapStr, error) {
// Refresh inode to process mapping (must be root).
if err := m.ptable.Refresh(); err != nil {
debugf("process table refresh had failures: %v", err)
}
// Send request over netlink and parse responses.
req := linux.NewInetDiagReq()
req.Header.Seq = atomic.AddUint32(&m.seq, 1)
sockets, err := linux.NetlinkInetDiagWithBuf(req, m.readBuffer, nil)
if err != nil {
return nil, errors.Wrap(err, "failed requesting socket dump")
}
debugf("netlink returned %d sockets", len(sockets))
// Filter sockets that were known during the previous poll.
sockets = m.filterAndRememberSockets(sockets)
// Enrich sockets with direction/pid/process/user/hostname and convert to MapStr.
rtn := make([]common.MapStr, 0, len(sockets))
for _, s := range sockets {
c := newConnection(s)
m.enrichConnectionData(c)
rtn = append(rtn, c.ToMapStr())
}
// Set the "previous" connections set to the "current" connections.
tmp := m.previousConns
m.previousConns = m.currentConns
m.currentConns = tmp.Reset()
// Reset the listeners for the next iteration.
m.listeners.Reset()
return rtn, nil
}
// filterAndRememberSockets filters sockets to remove sockets that were seen
// during the last poll. It stores all of the sockets it sees for the next
// poll.
func (m *MetricSet) filterAndRememberSockets(sockets ...[]*linux.InetDiagMsg) []*linux.InetDiagMsg {
var newSockets []*linux.InetDiagMsg
for _, list := range sockets {
for _, socket := range list {
// Register all listening sockets.
if socket.DstPort() == 0 {
m.listeners.Put(uint8(syscall.IPPROTO_TCP), socket.SrcIP(), socket.SrcPort())
}
// Filter known sockets.
if m.isNewSocket(socket) {
if logp.IsDebug(debugSelector) {
debugf("found new socket %v:%v -> %v:%v with state=%v, inode=%v, hash-id=%d",
socket.SrcIP(), socket.SrcPort(),
socket.DstIP(), socket.DstPort(),
linux.TCPState(socket.State), socket.Inode, socket.FastHash())
}
newSockets = append(newSockets, socket)
}
}
}
return newSockets
}
// isNewSocket returns true if the socket is new since the last poll.
func (m *MetricSet) isNewSocket(diag *linux.InetDiagMsg) bool {
// Don't use the socket's inode for deduplication because once the socket
// is closing the inode goes to 0.
key := diag.FastHash()
m.currentConns.Add(key)
return !m.previousConns.Contains(key)
}
// enrichConnectionData enriches the connection with username, direction,
// hostname of the remote IP (if enabled), eTLD + 1 of the hostname, and the
// process owning the socket.
func (m *MetricSet) enrichConnectionData(c *connection) {
c.Username = m.users.LookupUID(int(c.UID))
// Determine direction (incoming, outgoing, or listening).
c.Direction = m.listeners.Direction(uint8(syscall.IPPROTO_TCP),
c.LocalIP, c.LocalPort, c.RemoteIP, c.RemotePort)
// Reverse DNS lookup on the remote IP.
if m.reverseLookup != nil && c.Direction != Listening {
hostname, err := m.reverseLookup.Lookup(c.RemoteIP)
if err != nil {
c.DestHostError = err
} else {
c.DestHost = hostname
c.DestHostETLDPlusOne, _ = etldPlusOne(hostname)
}
}
// Add process info by finding the process that holds the socket's inode.
if proc := m.ptable.ProcessBySocketInode(c.Inode); proc != nil {
c.PID = proc.PID
c.Exe = proc.Executable
c.Command = proc.Command
c.CmdLine = proc.CmdLine
} else if m.euid == 0 {
if c.Inode == 0 {
c.ProcessError = fmt.Errorf("process has exited. inode=%v, tcp_state=%v",
c.Inode, c.State)
} else {
c.ProcessError = fmt.Errorf("process not found. inode=%v, tcp_state=%v",
c.Inode, c.State)
}
}
}
type connection struct {
Family linux.AddressFamily
LocalIP net.IP
LocalPort int
RemoteIP net.IP
RemotePort int
State linux.TCPState
Direction Direction
DestHost string // Reverse lookup of dest IP.
DestHostETLDPlusOne string
DestHostError error // Resolver error.
// Process identifiers.
Inode uint32 // Inode of the socket.
PID int // PID of the socket owner.
Exe string // Absolute path to the executable.
Command string // Command
CmdLine string // Full command line with arguments.
ProcessError error // Reason process info is unavailable.
// User identifiers.
UID uint32 // UID of the socket owner.
Username string // Username of the socket.
}
func newConnection(diag *linux.InetDiagMsg) *connection {
return &connection{
Family: linux.AddressFamily(diag.Family),
State: linux.TCPState(diag.State),
LocalIP: diag.SrcIP(),
LocalPort: diag.SrcPort(),
RemoteIP: diag.DstIP(),
RemotePort: diag.DstPort(),
Inode: diag.Inode,
UID: diag.UID,
PID: -1,
}
}
func (c *connection) ToMapStr() common.MapStr {
evt := common.MapStr{
"family": c.Family.String(),
"local": common.MapStr{
"ip": c.LocalIP.String(),
"port": c.LocalPort,
},
"user": common.MapStr{
"id": c.UID,
},
"direction": c.Direction.String(),
}
if c.Username != "" {
evt.Put("user.name", c.Username)
}
if c.ProcessError != nil {
evt.Put("process.error", c.ProcessError.Error())
} else {
process := common.MapStr{"pid": c.PID}
evt["process"] = process
if c.PID > 0 {
addOptionalString(process, "exe", c.Exe)
addOptionalString(process, "command", c.Command)
addOptionalString(process, "cmdline", c.CmdLine)
} else if c.PID == 0 {
process["command"] = "kernel"
}
}
if c.RemotePort != 0 {
remote := common.MapStr{
"ip": c.RemoteIP.String(),
"port": c.RemotePort,
}
evt["remote"] = remote
if c.DestHostError != nil {
remote["host_error"] = c.DestHostError.Error()
} else {
addOptionalString(remote, "host", c.DestHost)
addOptionalString(remote, "etld_plus_one", c.DestHostETLDPlusOne)
}
}
return evt
}
func addOptionalString(m common.MapStr, key, value string) {
if value == "" {
return
}
m[key] = value
}