v0.17.0 mtls added to network chain, sinks, and sources

This commit is contained in:
2026-08-29 18:44:44 -04:00
parent 5fbd5c71cf
commit dd665bb339
26 changed files with 2293 additions and 488 deletions
+74 -23
View File
@@ -6,12 +6,14 @@ import (
"crypto/tls"
"errors"
"fmt"
"maps"
"net"
"strconv"
"sync"
"sync/atomic"
"time"
"logwisp/internal/authz"
"logwisp/internal/chain"
"logwisp/internal/config"
"logwisp/internal/core"
@@ -50,6 +52,9 @@ type TCPChainSource struct {
tlsConfig *tls.Config
tlsHandshakeErrors atomic.Uint64
// Authorization
auth *authz.Policy
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
@@ -91,6 +96,10 @@ func NewTCPChainSourcePlugin(
if err != nil {
return nil, err
}
authPolicy, err := authz.New(opts.Auth, opts.TLS, authz.RoleChainListener)
if err != nil {
return nil, err
}
s := &TCPChainSource{
id: id,
@@ -100,6 +109,7 @@ func NewTCPChainSourcePlugin(
conns: make(map[net.Conn]struct{}),
logger: logger,
tlsConfig: tlsCfg,
auth: authPolicy,
}
s.lastEntryTime.Store(time.Time{})
@@ -109,7 +119,21 @@ func NewTCPChainSourcePlugin(
"host", opts.Host,
"port", opts.Port,
"tls", tlsCfg != nil,
"mtls", tlsCfg != nil && tlsCfg.ClientAuth == tls.RequireAndVerifyClientCert)
"mtls", tlsCfg != nil && tlsCfg.ClientAuth == tls.RequireAndVerifyClientCert,
"auth", authPolicy.Describe())
if authPolicy.Unrestricted() {
logger.Warn("msg", "Auth policy admits any identity the configured CA vouches for",
"component", "tcp_chain_source",
"instance_id", id,
"hint", "set auth.allow or auth.allow_patterns to authorize named peers")
}
if authPolicy.BindsNode() {
logger.Info("msg", "Node labels bound to peer identity; trust_node is ignored",
"component", "tcp_chain_source",
"instance_id", id,
"node_binding", authPolicy.NodeBinding(),
"trust_node", opts.TrustNode)
}
return s, nil
}
@@ -118,9 +142,9 @@ func (s *TCPChainSource) Capabilities() []core.Capability {
caps := []core.Capability{core.CapSessionAware, core.CapMultiSession}
if s.tlsConfig != nil {
caps = append(caps, core.CapTLS)
if s.tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
caps = append(caps, core.CapAuth) // mTLS is authentication
}
}
if s.auth.Enabled() {
caps = append(caps, core.CapAuth) // authorizes peers, not just the CA
}
return caps
}
@@ -191,6 +215,18 @@ func (s *TCPChainSource) Stop() {
// GetStats returns the source's statistics
func (s *TCPChainSource) GetStats() source.SourceStats {
lastEntry, _ := s.lastEntryTime.Load().(time.Time)
details := map[string]any{
"host": s.config.Host,
"port": s.config.Port,
"tls": s.tlsConfig != nil,
"tls_handshake_errors": s.tlsHandshakeErrors.Load(),
"active_connections": s.activeConns.Load(),
"rejected_conns": s.rejectedConns.Load(),
"parse_errors": s.parseErrors.Load(),
"trust_node": s.config.TrustNode,
}
maps.Copy(details, s.auth.Stats())
return source.SourceStats{
ID: s.id,
Type: "tcp_chain",
@@ -198,16 +234,7 @@ func (s *TCPChainSource) GetStats() source.SourceStats {
DroppedEntries: s.droppedEntries.Load(),
StartTime: s.startTime,
LastEntryTime: lastEntry,
Details: map[string]any{
"host": s.config.Host,
"port": s.config.Port,
"tls": s.tlsConfig != nil,
"tls_handshake_errors": s.tlsHandshakeErrors.Load(),
"active_connections": s.activeConns.Load(),
"rejected_conns": s.rejectedConns.Load(),
"parse_errors": s.parseErrors.Load(),
"trust_node": s.config.TrustNode,
},
Details: details,
}
}
@@ -276,6 +303,18 @@ func (s *TCPChainSource) handleConn(conn net.Conn) {
tlsState = &cs
}
// Authorize before a preamble is parsed on an unauthorized peer's behalf
ident, err := s.auth.Authorize(tlsState)
if err != nil {
s.rejectedConns.Add(1)
s.logger.Warn("msg", "Connection rejected by auth policy",
"component", "tcp_chain_source",
"instance_id", s.id,
"remote_addr", remote,
"error", err)
return // deferred cleanup closes conn
}
scanner := bufio.NewScanner(conn)
// Oversized line (> MaxLogEntryBytes) is a protocol violation; scanner is
// unrecoverable after ErrTooLong, connection terminates
@@ -299,14 +338,24 @@ func (s *TCPChainSource) handleConn(conn net.Conn) {
return
}
connNode := hello.Node
if connNode == "" || !s.config.TrustNode {
if host, _, splitErr := net.SplitHostPort(remote); splitErr == nil {
connNode = host
} else {
connNode = remote
}
fallbackNode := remote
if host, _, splitErr := net.SplitHostPort(remote); splitErr == nil {
fallbackNode = host
}
connNode, err := s.auth.ResolveNode(hello.Node, fallbackNode, s.config.TrustNode, ident)
if err != nil {
s.rejectedConns.Add(1)
s.logger.Warn("msg", "Connection rejected by node binding",
"component", "tcp_chain_source",
"instance_id", s.id,
"remote_addr", remote,
"declared_node", hello.Node,
"error", err)
return
}
// force relabels every entry, so an edge cannot smuggle a foreign origin
// through the per-entry node field either
trustEntryNode := s.auth.TrustsEntryNode(s.config.TrustNode)
meta := map[string]any{
"type": "tcp_chain",
@@ -318,13 +367,15 @@ func (s *TCPChainSource) handleConn(conn net.Conn) {
meta["tls_peer_cn"] = cn
}
}
ident.Apply(meta)
sess := s.proxy.CreateSession(remote, meta)
sessID = sess.ID
s.logger.Info("msg", "Chain connection established",
"component", "tcp_chain_source",
"remote_addr", remote,
"node", connNode)
"node", connNode,
"auth_identity", ident.Name)
idle := time.Duration(s.config.ReadTimeoutMS) * time.Millisecond
for {
@@ -348,7 +399,7 @@ func (s *TCPChainSource) handleConn(conn net.Conn) {
}
s.proxy.UpdateActivity(sessID)
entry, err := chain.DecodeEntry(line, connNode, s.config.TrustNode)
entry, err := chain.DecodeEntry(line, connNode, trustEntryNode)
if err != nil {
s.parseErrors.Add(1)
s.logger.Debug("msg", "Dropped malformed chain entry",