mirror of
https://github.com/tailscale/tailscale.git
synced 2026-02-11 10:41:43 +01:00
This converts the info and localUser fields on the conn from pointers to values. I consider this an overall improvement since both structs are small and it makes access safer in cases when they've not yet been set. Updates tailscale/corp#36268 Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
177 lines
4.7 KiB
Go
177 lines
4.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux && !android
|
|
|
|
package tailssh
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"syscall"
|
|
|
|
"golang.org/x/sys/unix"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
const (
|
|
auditUserLogin = 1112 // audit message type for user login (from linux/audit.h)
|
|
netlinkAudit = 9 // AF_NETLINK protocol number for audit (from linux/netlink.h)
|
|
nlmFRequest = 0x01 // netlink message flag: request (from linux/netlink.h)
|
|
|
|
// maxAuditMessageLength is the maximum length of an audit message payload.
|
|
// This is derived from MAX_AUDIT_MESSAGE_LENGTH (8970) in the Linux kernel
|
|
// (linux/audit.h), minus overhead for the netlink header and safety margin.
|
|
maxAuditMessageLength = 8192
|
|
)
|
|
|
|
// hasAuditWriteCap checks if the process has CAP_AUDIT_WRITE in its effective capability set.
|
|
func hasAuditWriteCap() bool {
|
|
var hdr unix.CapUserHeader
|
|
var data [2]unix.CapUserData
|
|
|
|
hdr.Version = unix.LINUX_CAPABILITY_VERSION_3
|
|
hdr.Pid = int32(os.Getpid())
|
|
|
|
if err := unix.Capget(&hdr, &data[0]); err != nil {
|
|
return false
|
|
}
|
|
|
|
const capBit = uint32(1 << (unix.CAP_AUDIT_WRITE % 32))
|
|
const capIdx = unix.CAP_AUDIT_WRITE / 32
|
|
return (data[capIdx].Effective & capBit) != 0
|
|
}
|
|
|
|
// buildAuditNetlinkMessage constructs a netlink audit message.
|
|
// This is separated from sendAuditMessage to allow testing the message format
|
|
// without requiring CAP_AUDIT_WRITE or a netlink socket.
|
|
func buildAuditNetlinkMessage(msgType uint16, message string) ([]byte, error) {
|
|
msgBytes := []byte(message)
|
|
if len(msgBytes) > maxAuditMessageLength {
|
|
msgBytes = msgBytes[:maxAuditMessageLength]
|
|
}
|
|
msgLen := len(msgBytes)
|
|
|
|
totalLen := syscall.NLMSG_HDRLEN + msgLen
|
|
alignedLen := (totalLen + syscall.NLMSG_ALIGNTO - 1) & ^(syscall.NLMSG_ALIGNTO - 1)
|
|
|
|
nlh := syscall.NlMsghdr{
|
|
Len: uint32(totalLen),
|
|
Type: msgType,
|
|
Flags: nlmFRequest,
|
|
Seq: 1,
|
|
Pid: uint32(os.Getpid()),
|
|
}
|
|
|
|
buf := bytes.NewBuffer(make([]byte, 0, alignedLen))
|
|
if err := binary.Write(buf, binary.NativeEndian, nlh); err != nil {
|
|
return nil, err
|
|
}
|
|
buf.Write(msgBytes)
|
|
|
|
for buf.Len() < alignedLen {
|
|
buf.WriteByte(0)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// sendAuditMessage sends a message to the audit subsystem using raw netlink.
|
|
// It logs errors but does not return them.
|
|
func sendAuditMessage(logf logger.Logf, msgType uint16, message string) {
|
|
if !hasAuditWriteCap() {
|
|
return
|
|
}
|
|
|
|
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, netlinkAudit)
|
|
if err != nil {
|
|
logf("auditd: failed to create netlink socket: %v", err)
|
|
return
|
|
}
|
|
defer syscall.Close(fd)
|
|
|
|
bindAddr := &syscall.SockaddrNetlink{
|
|
Family: syscall.AF_NETLINK,
|
|
Pid: uint32(os.Getpid()),
|
|
Groups: 0,
|
|
}
|
|
|
|
if err := syscall.Bind(fd, bindAddr); err != nil {
|
|
logf("auditd: failed to bind netlink socket: %v", err)
|
|
return
|
|
}
|
|
|
|
kernelAddr := &syscall.SockaddrNetlink{
|
|
Family: syscall.AF_NETLINK,
|
|
Pid: 0,
|
|
Groups: 0,
|
|
}
|
|
|
|
msgBytes, err := buildAuditNetlinkMessage(msgType, message)
|
|
if err != nil {
|
|
logf("auditd: failed to build audit message: %v", err)
|
|
return
|
|
}
|
|
|
|
if err := syscall.Sendto(fd, msgBytes, 0, kernelAddr); err != nil {
|
|
logf("auditd: failed to send audit message: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// logSSHLogin logs an SSH login event to auditd with whois information.
|
|
func logSSHLogin(logf logger.Logf, c *conn) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
|
|
exePath := c.srv.tailscaledPath
|
|
if exePath == "" {
|
|
exePath = "tailscaled"
|
|
}
|
|
|
|
srcIP := c.info.src.Addr().String()
|
|
srcPort := c.info.src.Port()
|
|
dstIP := c.info.dst.Addr().String()
|
|
dstPort := c.info.dst.Port()
|
|
|
|
tailscaleUser := c.info.uprof.LoginName
|
|
tailscaleUserID := c.info.uprof.ID
|
|
tailscaleDisplayName := c.info.uprof.DisplayName
|
|
nodeName := c.info.node.Name()
|
|
nodeID := c.info.node.ID()
|
|
|
|
localUser := c.localUser.Username
|
|
localUID := c.localUser.Uid
|
|
localGID := c.localUser.Gid
|
|
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
hostname = "unknown"
|
|
}
|
|
|
|
// use principally the same format as ssh / PAM, which come from the audit userspace, i.e.
|
|
// https://github.com/linux-audit/audit-userspace/blob/b6f8c208435038df113a9795e3e202720aee6b70/lib/audit_logging.c#L515
|
|
msg := fmt.Sprintf(
|
|
"op=login acct=%s uid=%s gid=%s "+
|
|
"src=%s src_port=%d dst=%s dst_port=%d "+
|
|
"hostname=%q exe=%q terminal=ssh res=success "+
|
|
"ts_user=%q ts_user_id=%d ts_display_name=%q ts_node=%q ts_node_id=%d",
|
|
localUser, localUID, localGID,
|
|
srcIP, srcPort, dstIP, dstPort,
|
|
hostname, exePath,
|
|
tailscaleUser, tailscaleUserID, tailscaleDisplayName, nodeName, nodeID,
|
|
)
|
|
|
|
sendAuditMessage(logf, auditUserLogin, msg)
|
|
|
|
logf("audit: SSH login: user=%s uid=%s from=%s ts_user=%s node=%s",
|
|
localUser, localUID, srcIP, tailscaleUser, nodeName)
|
|
}
|
|
|
|
func init() {
|
|
hookSSHLoginSuccess.Set(logSSHLogin)
|
|
}
|