// 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 || c.info == nil || c.localUser == 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)
}
