// Package tlsx builds crypto/tls configurations from config.TLSOptions. // It is the single seam between declarative TLS config and the stdlib; // each network plugin calls exactly one constructor. package tlsx import ( "crypto/tls" "crypto/x509" "fmt" stdlog "log" "os" "strings" "time" "logwisp/internal/config" "github.com/lixenwraith/log" ) // HandshakeTimeout bounds TLS handshakes on both accept and dial paths const HandshakeTimeout = 10 * time.Second // Server builds the *tls.Config for listener plugins // (tcp/http sinks, tcp_chain/http_chain sources). Returns (nil, nil) when disabled. func Server(o *config.TLSOptions) (*tls.Config, error) { if o == nil || !o.Enabled { return nil, nil } if o.CertFile == "" || o.KeyFile == "" { return nil, fmt.Errorf("tls: cert_file and key_file are required for listeners") } cert, err := tls.LoadX509KeyPair(o.CertFile, o.KeyFile) if err != nil { return nil, fmt.Errorf("tls: load keypair: %w", err) } mv, err := minVersion(o.MinVersion) if err != nil { return nil, err } cfg := &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: mv, } if o.ClientAuth { if o.ClientCAFile == "" { return nil, fmt.Errorf("tls: client_auth requires client_ca_file") } pool, err := loadPool(o.ClientCAFile) if err != nil { return nil, err } cfg.ClientCAs = pool cfg.ClientAuth = tls.RequireAndVerifyClientCert } return cfg, nil } // Client builds the *tls.Config for dialer plugins (tcp_chain/http_chain // sinks). host seeds ServerName when no override is set; Go verifies IP SANs // when host is an address. Returns (nil, nil) when disabled. func Client(o *config.TLSOptions, host string) (*tls.Config, error) { if o == nil || !o.Enabled { return nil, nil } mv, err := minVersion(o.MinVersion) if err != nil { return nil, err } cfg := &tls.Config{ MinVersion: mv, ServerName: o.ServerName, InsecureSkipVerify: o.InsecureSkipVerify, } if cfg.ServerName == "" { cfg.ServerName = host } if o.CAFile != "" { pool, err := loadPool(o.CAFile) if err != nil { return nil, err } cfg.RootCAs = pool } if (o.CertFile == "") != (o.KeyFile == "") { return nil, fmt.Errorf("tls: cert_file and key_file must be set together") } if o.CertFile != "" { cert, err := tls.LoadX509KeyPair(o.CertFile, o.KeyFile) if err != nil { return nil, fmt.Errorf("tls: load keypair: %w", err) } cfg.Certificates = []tls.Certificate{cert} } return cfg, nil } // PeerCN returns the subject CN of the verified peer leaf, "" if none func PeerCN(cs tls.ConnectionState) string { if len(cs.PeerCertificates) == 0 { return "" } return cs.PeerCertificates[0].Subject.CommonName } // HTTPErrorLog adapts the structured logger for http.Server.ErrorLog so TLS // handshake failures don't bypass log routing straight to stderr (which would // violate the console sanitization policy). func HTTPErrorLog(l *log.Logger, component string) *stdlog.Logger { return stdlog.New(errLogWriter{l: l, component: component}, "", 0) } type errLogWriter struct { l *log.Logger component string } func (w errLogWriter) Write(p []byte) (int, error) { w.l.Warn("msg", strings.TrimSpace(string(p)), "component", w.component) return len(p), nil } func loadPool(file string) (*x509.CertPool, error) { pemBytes, err := os.ReadFile(file) if err != nil { return nil, fmt.Errorf("tls: read CA file: %w", err) } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pemBytes) { return nil, fmt.Errorf("tls: no certificates found in %s", file) } return pool, nil } func minVersion(s string) (uint16, error) { switch s { case "", "1.3": return tls.VersionTLS13, nil case "1.2": return tls.VersionTLS12, nil default: return 0, fmt.Errorf("tls: min_version %q (valid: \"1.2\", \"1.3\")", s) } }