v0.16.0 tls added to tcp/http sources and sinks

This commit is contained in:
2026-07-18 04:10:22 -04:00
parent f0aca019f3
commit 325b51d840
8 changed files with 526 additions and 165 deletions
+41 -11
View File
@@ -3,6 +3,7 @@ package httpchain
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"net"
@@ -19,6 +20,7 @@ import (
"logwisp/internal/plugin"
"logwisp/internal/session"
"logwisp/internal/source"
"logwisp/internal/tlsx"
lconfig "github.com/lixenwraith/config"
"github.com/lixenwraith/log"
@@ -49,6 +51,9 @@ type HTTPChainSource struct {
server *http.Server
logger *log.Logger
// TLS
tlsConfig *tls.Config
// Session cache: one session per remote host + declared node
sessions map[string]string // key -> sessionID
sessionsMu sync.Mutex
@@ -95,6 +100,10 @@ func NewHTTPChainSourcePlugin(
if opts.ReadTimeoutMS <= 0 {
opts.ReadTimeoutMS = DefaultHTTPChainSourceReadTimeoutMS
}
tlsCfg, err := tlsx.Server(opts.TLS)
if err != nil {
return nil, err
}
s := &HTTPChainSource{
id: id,
@@ -103,6 +112,7 @@ func NewHTTPChainSourcePlugin(
subscribers: make([]chan core.LogEntry, 0),
sessions: make(map[string]string),
logger: logger,
tlsConfig: tlsCfg,
}
s.lastEntryTime.Store(time.Time{})
@@ -111,17 +121,22 @@ func NewHTTPChainSourcePlugin(
"instance_id", id,
"host", opts.Host,
"port", opts.Port,
"ingest_path", opts.IngestPath)
"ingest_path", opts.IngestPath,
"tls", tlsCfg != nil,
"mtls", tlsCfg != nil && tlsCfg.ClientAuth == tls.RequireAndVerifyClientCert)
return s, nil
}
// Capabilities returns supported capabilities
func (s *HTTPChainSource) Capabilities() []core.Capability {
// CapTLS/CapAuth added when transport security lands
return []core.Capability{
core.CapSessionAware,
core.CapMultiSession,
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
}
}
return caps
}
// Subscribe returns a channel for receiving log entries
@@ -150,12 +165,19 @@ func (s *HTTPChainSource) Start() error {
Handler: mux,
ReadTimeout: time.Duration(s.config.ReadTimeoutMS) * time.Millisecond,
ReadHeaderTimeout: HTTPChainReadHeaderTimeout,
// Future: TLSConfig for transport security
// TLS handshake bounded by min(ReadTimeout, ReadHeaderTimeout)
ErrorLog: tlsx.HTTPErrorLog(s.logger, "http_chain_source"),
}
s.startTime = time.Now()
serve := s.server.Serve
if s.tlsConfig != nil {
s.server.TLSConfig = s.tlsConfig
serve = func(l net.Listener) error { return s.server.ServeTLS(l, "", "") }
}
go func() {
if err := s.server.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
if err := serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("msg", "HTTP chain server terminated",
"component", "http_chain_source",
"instance_id", s.id,
@@ -215,6 +237,7 @@ func (s *HTTPChainSource) GetStats() source.SourceStats {
"host": s.config.Host,
"port": s.config.Port,
"ingest_path": s.config.IngestPath,
"tls": s.tlsConfig != nil,
"total_requests": s.totalRequests.Load(),
"rejected_requests": s.rejectedRequests.Load(),
"parse_errors": s.parseErrors.Load(),
@@ -281,14 +304,14 @@ func (s *HTTPChainSource) handleIngest(w http.ResponseWriter, r *http.Request) {
for _, entry := range entries {
s.publish(entry)
}
s.proxy.UpdateActivity(s.sessionFor(remoteHost, connNode))
s.proxy.UpdateActivity(s.sessionFor(remoteHost, connNode, r.TLS))
w.Header().Set(chain.HeaderAccepted, strconv.Itoa(len(entries)))
w.WriteHeader(http.StatusNoContent)
}
// sessionFor returns the cached session for a remote+node, recreating after idle expiry
func (s *HTTPChainSource) sessionFor(remoteHost, node string) string {
func (s *HTTPChainSource) sessionFor(remoteHost, node string, cs *tls.ConnectionState) string {
key := remoteHost + "|" + node
s.sessionsMu.Lock()
defer s.sessionsMu.Unlock()
@@ -298,10 +321,17 @@ func (s *HTTPChainSource) sessionFor(remoteHost, node string) string {
return id
}
}
sess := s.proxy.CreateSession(remoteHost, map[string]any{
meta := map[string]any{
"type": "http_chain",
"node": node,
})
}
if cs != nil {
meta["tls"] = true
if cn := tlsx.PeerCN(*cs); cn != "" {
meta["tls_peer_cn"] = cn
}
}
sess := s.proxy.CreateSession(remoteHost, meta)
s.sessions[key] = sess.ID
return sess.ID
}
+60 -35
View File
@@ -3,7 +3,7 @@ package tcpchain
import (
"bufio"
"context"
"encoding/json"
"crypto/tls"
"errors"
"fmt"
"net"
@@ -18,6 +18,7 @@ import (
"logwisp/internal/plugin"
"logwisp/internal/session"
"logwisp/internal/source"
"logwisp/internal/tlsx"
lconfig "github.com/lixenwraith/config"
"github.com/lixenwraith/log"
@@ -45,6 +46,10 @@ type TCPChainSource struct {
conns map[net.Conn]struct{}
logger *log.Logger
// TLS
tlsConfig *tls.Config
tlsHandshakeErrors atomic.Uint64
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
@@ -82,6 +87,10 @@ func NewTCPChainSourcePlugin(
if opts.HelloTimeoutMS <= 0 {
opts.HelloTimeoutMS = DefaultChainSourceHelloTimeoutMS
}
tlsCfg, err := tlsx.Server(opts.TLS)
if err != nil {
return nil, err
}
s := &TCPChainSource{
id: id,
@@ -90,6 +99,7 @@ func NewTCPChainSourcePlugin(
subscribers: make([]chan core.LogEntry, 0),
conns: make(map[net.Conn]struct{}),
logger: logger,
tlsConfig: tlsCfg,
}
s.lastEntryTime.Store(time.Time{})
@@ -97,17 +107,22 @@ func NewTCPChainSourcePlugin(
"component", "tcp_chain_source",
"instance_id", id,
"host", opts.Host,
"port", opts.Port)
"port", opts.Port,
"tls", tlsCfg != nil,
"mtls", tlsCfg != nil && tlsCfg.ClientAuth == tls.RequireAndVerifyClientCert)
return s, nil
}
// Capabilities returns supported capabilities
func (s *TCPChainSource) Capabilities() []core.Capability {
// CapTLS/CapAuth added when transport security lands
return []core.Capability{
core.CapSessionAware,
core.CapMultiSession,
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
}
}
return caps
}
// Subscribe returns a channel for receiving log entries
@@ -122,11 +137,15 @@ func (s *TCPChainSource) Subscribe() <-chan core.LogEntry {
// Start binds the listener and begins accepting connections
func (s *TCPChainSource) Start() error {
addr := net.JoinHostPort(s.config.Host, strconv.FormatInt(s.config.Port, 10))
// IPv4-only
// IPv4-only. TLS-wrapped when configured; handshake runs explicitly in
// handleConn under tlsx.HandshakeTimeout, pre-hello.
ln, err := net.Listen("tcp4", addr)
if err != nil {
return fmt.Errorf("listen %s: %w", addr, err)
}
if s.tlsConfig != nil {
ln = tls.NewListener(ln, s.tlsConfig)
}
s.listener = ln
s.ctx, s.cancel = context.WithCancel(context.Background())
s.startTime = time.Now()
@@ -180,12 +199,14 @@ func (s *TCPChainSource) GetStats() source.SourceStats {
StartTime: s.startTime,
LastEntryTime: lastEntry,
Details: map[string]any{
"host": s.config.Host,
"port": s.config.Port,
"active_connections": s.activeConns.Load(),
"rejected_conns": s.rejectedConns.Load(),
"parse_errors": s.parseErrors.Load(),
"trust_node": s.config.TrustNode,
"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,
},
}
}
@@ -238,6 +259,23 @@ func (s *TCPChainSource) handleConn(conn net.Conn) {
s.activeConns.Add(-1)
}()
var tlsState *tls.ConnectionState
if tc, ok := conn.(*tls.Conn); ok {
hctx, cancel := context.WithTimeout(s.ctx, tlsx.HandshakeTimeout)
err := tc.HandshakeContext(hctx)
cancel()
if err != nil {
s.tlsHandshakeErrors.Add(1)
s.logger.Warn("msg", "TLS handshake failed",
"component", "tcp_chain_source",
"remote_addr", remote,
"error", err)
return // deferred cleanup closes conn
}
cs := tc.ConnectionState()
tlsState = &cs
}
scanner := bufio.NewScanner(conn)
// Oversized line (> MaxLogEntryBytes) is a protocol violation; scanner is
// unrecoverable after ErrTooLong, connection terminates
@@ -270,10 +308,17 @@ func (s *TCPChainSource) handleConn(conn net.Conn) {
}
}
sess := s.proxy.CreateSession(remote, map[string]any{
meta := map[string]any{
"type": "tcp_chain",
"node": connNode,
})
}
if tlsState != nil {
meta["tls"] = true
if cn := tlsx.PeerCN(*tlsState); cn != "" {
meta["tls_peer_cn"] = cn
}
}
sess := s.proxy.CreateSession(remote, meta)
sessID = sess.ID
s.logger.Info("msg", "Chain connection established",
@@ -315,26 +360,6 @@ func (s *TCPChainSource) handleConn(conn net.Conn) {
}
}
// parseEntry decodes a canonical LogEntry line and applies the node policy
func (s *TCPChainSource) parseEntry(line []byte, connNode string) (core.LogEntry, bool) {
var entry core.LogEntry
if err := json.Unmarshal(line, &entry); err != nil {
s.parseErrors.Add(1)
s.logger.Debug("msg", "Dropped malformed chain entry",
"component", "tcp_chain_source",
"error", err)
return core.LogEntry{}, false
}
if entry.Time.IsZero() {
entry.Time = time.Now()
}
if entry.Node == "" || !s.config.TrustNode {
entry.Node = connNode
}
entry.RawSize = int64(len(line))
return entry, true
}
// publish sends a log entry to all subscribers
func (s *TCPChainSource) publish(entry core.LogEntry) {
s.mu.RLock()