# Sentinel

Sentinel (`ja4sentinel`) is a Go daemon that performs live network packet capture on a Linux server, extracts TLS ClientHello handshakes, generates JA4 and JA3 fingerprints, enriches them with IP/TCP metadata, and outputs structured JSON log records to configurable destinations (UNIX socket, file, or stdout).

## Role in the Pipeline

Sentinel is the **network-layer ingestion point**. It sits on the target server, captures TLS traffic via libpcap, and feeds fingerprinted events to the [correlator](correlator.md) through a UNIX datagram socket.

```
Network traffic (port 443/8443)
        │ pcap
        ▼
┌───────────────┐
│   sentinel    │
│  ┌─────────┐  │
│  │ capture  │──▶ Raw packets
│  └─────────┘  │
│  ┌─────────┐  │
│  │ tlsparse│──▶ TLS ClientHello extraction + TCP reassembly
│  └─────────┘  │
│  ┌─────────┐  │
│  │ finger- │──▶ JA4/JA3 fingerprint generation
│  │ print   │  │
│  └─────────┘  │
│  ┌─────────┐  │
│  │ output  │──▶ UNIX socket / file / stdout
│  └─────────┘  │
└───────────────┘
```

## Architecture

Sentinel uses a pipeline of goroutines:

1. **Capture goroutine** — Opens pcap handle on the configured interface, applies BPF filter, reads raw packets into a buffered channel (`packet_buffer_size`).
2. **Packet processor goroutine** — Reads from the channel, feeds packets to the TLS parser, generates fingerprints, and writes output.
3. **Watchdog goroutine** — Sends systemd watchdog heartbeats at half the configured interval.
4. **Signal handler** — Listens for `SIGINT`/`SIGTERM` (graceful shutdown) and `SIGHUP` (log rotation).

### Key Interfaces

| Interface | Package | Description |
|-----------|---------|-------------|
| `Capture` | `internal/capture` | Packet capture via libpcap |
| `Parser` | `internal/tlsparse` | TCP reassembly + ClientHello extraction |
| `Engine` | `internal/fingerprint` | JA4/JA3 fingerprint generation |
| `Writer` | `internal/output` | Log record output (stdout, file, UNIX socket) |
| `MultiWriter` | `internal/output` | Fan-out to multiple writers |
| `Builder` | `internal/output` | Factory for constructing writers from config |

## Configuration Reference

Configuration is loaded from a YAML file (default: `config.yml`) with environment variable overrides.

### Core Settings

| Name | Type | Default | Env Override | Description |
|------|------|---------|-------------|-------------|
| `core.interface` | string | `any` | `JA4SENTINEL_INTERFACE` | Network interface to capture (`any` = all interfaces) |
| `core.listen_ports` | []uint16 | `[443]` | `JA4SENTINEL_PORTS` | TCP ports to monitor (comma-separated in env) |
| `core.bpf_filter` | string | `""` (auto) | `JA4SENTINEL_BPF_FILTER` | Custom BPF filter (empty = auto-generated) |
| `core.local_ips` | []string | `[]` (auto) | — | Local IPs to monitor (empty = auto-detect, excludes loopback) |
| `core.exclude_source_ips` | []string | `[]` | — | Source IPs or CIDRs to exclude (e.g., `["10.0.0.0/8"]`) |
| `core.flow_timeout_sec` | int | `30` | `JA4SENTINEL_FLOW_TIMEOUT` | Timeout for TLS handshake extraction (1–300) |
| `core.packet_buffer_size` | int | `1000` | `JA4SENTINEL_PACKET_BUFFER_SIZE` | Packet channel buffer size (1–1,000,000) |
| `core.log_level` | string | `info` | — | Log level: `debug`, `info`, `warn`, `error` (YAML only) |

> **Note:** `log_level` is intentionally not overridable via environment variable (architecture decision since v1.1.12).

### Output Settings

Each output is an entry in the `outputs` array:

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `type` | string | — | Output type: `unix_socket`, `stdout`, `file` |
| `enabled` | bool | — | Whether this output is active |
| `async_buffer` | int | `1000` | Queue size for async writes |
| `params.socket_path` | string | — | Path for `unix_socket` type |
| `params.path` | string | — | File path for `file` type |

### Example Configuration

```yaml
core:
  interface: any
  listen_ports: [443, 8443]
  bpf_filter: ""
  local_ips: []
  exclude_source_ips: ["10.0.0.0/8", "192.168.1.1"]
  flow_timeout_sec: 30
  packet_buffer_size: 1000
  log_level: info

outputs:
  - type: unix_socket
    enabled: true
    params:
      socket_path: /var/run/logcorrelator/network.socket
  - type: file
    enabled: false
    params:
      path: /var/log/ja4sentinel/ja4.log
```

## Output Format (LogRecord JSON Schema)

Each output record is a flat JSON object:

```json
{
  "src_ip": "203.0.113.42",
  "src_port": 52341,
  "dst_ip": "192.168.1.10",
  "dst_port": 443,
  "ip_meta_ttl": 64,
  "ip_meta_total_length": 583,
  "ip_meta_id": 12345,
  "ip_meta_df": true,
  "tcp_meta_window_size": 65535,
  "tcp_meta_mss": 1460,
  "tcp_meta_window_scale": 8,
  "tcp_meta_options": "MSS,NOP,WScale,NOP,NOP,Timestamps,SACK",
  "conn_id": "203.0.113.42:52341-192.168.1.10:443",
  "sensor_id": "",
  "tls_version": "1.3",
  "tls_sni": "example.com",
  "tls_alpn": "h2",
  "syn_to_clienthello_ms": 12,
  "ja4": "t13d1516h2_8daaf6152771_b0da82dd1658",
  "ja3": "771,4866-4867-4865-49196-49200...",
  "ja3_hash": "e7d705a3286e19ea42f587b344ee6865",
  "timestamp": 1709312345678901234
}
```

### Field Reference

| Field | Type | Description |
|-------|------|-------------|
| `src_ip` | string | Client source IP address |
| `src_port` | uint16 | Client source port |
| `dst_ip` | string | Server destination IP address |
| `dst_port` | uint16 | Server destination port |
| `ip_meta_ttl` | uint8 | IP Time-To-Live |
| `ip_meta_total_length` | uint16 | IP total packet length |
| `ip_meta_id` | uint16 | IP identification field |
| `ip_meta_df` | bool | IP Don't Fragment flag |
| `tcp_meta_window_size` | uint16 | TCP window size |
| `tcp_meta_mss` | uint16 | TCP Maximum Segment Size (omitted if 0) |
| `tcp_meta_window_scale` | uint8 | TCP window scale factor (omitted if 0) |
| `tcp_meta_options` | string | Comma-separated TCP options |
| `conn_id` | string | Unique flow identifier |
| `sensor_id` | string | Sensor/captor identifier |
| `tls_version` | string | Max TLS version from ClientHello |
| `tls_sni` | string | Server Name Indication |
| `tls_alpn` | string | ALPN protocol (e.g., `h2`, `http/1.1`) |
| `syn_to_clienthello_ms` | uint32 | Time from SYN to ClientHello (ms) |
| `ja4` | string | JA4 TLS fingerprint |
| `ja3` | string | JA3 TLS fingerprint |
| `ja3_hash` | string | MD5 hash of JA3 string |
| `timestamp` | int64 | Unix nanoseconds |

## UNIX Socket Output Protocol

- **Socket type**: `unixgram` (DGRAM — connectionless)
- **Encoding**: One JSON object per datagram (no delimiter)
- **Max datagram size**: 64 KB
- **Reconnection**: Exponential backoff (100 ms → 2 s), max 3 attempts per write
- **Queue**: Async write queue (default 1000 items) absorbs transient socket failures
- **Error callback**: Consecutive failures are tracked and reported

## Signal Handling

| Signal | Behavior |
|--------|----------|
| `SIGTERM` / `SIGINT` | Graceful shutdown: cancel context, close capture, flush outputs, log filter stats |
| `SIGHUP` | Log rotation: reopen file outputs (used by `systemctl reload` + logrotate) |

## JA4 Fingerprint Algorithm

1. Extract TLS ClientHello from the TCP payload (with TCP reassembly for fragmented handshakes)
2. Parse cipher suites, extensions, ALPN, SNI, supported versions
3. Build JA4 string: `t{version}{sni_flag}{cipher_count}{ext_count}_{cipher_hash}_{ext_hash}`
4. Build JA3 string: `{version},{ciphers},{extensions},{curves},{formats}`
5. Compute JA3 MD5 hash

Sentinel uses the `tlsfingerprint` library for ALPN and TLS version parsing, with custom sanitization for malformed/truncated ClientHellos.

## Deployment

### systemd

```ini
[Unit]
Description=ja4sentinel TLS fingerprinting daemon
After=network.target

[Service]
Type=notify
ExecStart=/usr/bin/ja4sentinel -config /etc/ja4sentinel/config.yml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
WatchdogSec=30
TimeoutStopSec=2

[Install]
WantedBy=multi-user.target
```

Sentinel uses systemd `sd_notify` for:
- `READY` — sent after initialization
- `WATCHDOG` — sent at half the `WatchdogSec` interval
- `STOPPING` — sent before shutdown

### Docker

```bash
make build-sentinel
docker run --cap-add=NET_RAW --cap-add=NET_ADMIN \
  -v /var/run/logcorrelator:/var/run/logcorrelator \
  ja4-platform/sentinel:latest
```

## RPM Package Contents

| Path | Description |
|------|-------------|
| `/usr/bin/ja4sentinel` | Binary (statically linked Go) |
| `/etc/ja4sentinel/config.yml.default` | Default configuration (noreplace) |
| `/usr/share/ja4sentinel/config.yml` | Reference configuration |
| `/usr/lib/systemd/system/ja4sentinel.service` | systemd unit |
| `/etc/logrotate.d/ja4sentinel` | logrotate configuration |
| `/var/lib/ja4sentinel/` | State directory |
| `/var/log/ja4sentinel/` | Log directory |
| `/var/run/logcorrelator/` | Socket directory |

### RPM Dependencies

- `systemd`
- `libpcap >= 1.9.0`

### Supported Distributions

- Rocky Linux 8, 9, 10
- AlmaLinux 8, 9
- RHEL 8, 9
