feat: add comprehensive TLS certificate SAN support with wildcard domains

- Add RFC-compliant wildcard certificates (*.api.bose.io, *.api.bosecm.com) for automatic API coverage
- Include additional Bose production domains (worldwide.bose.com, music.api.bose.com, bose-prod.apigee.net)
- Implement TLS certificate request logging and wildcard domain matching logic
- Add detailed TLS handshake debugging with connection state tracking
- Wrap TLS listener with logging to capture certificate selection and handshake failures
- Update documentation with wildcard certificate coverage and debugging features
- Normalize test data to use consistent local IP addresses

This enables automatic coverage of all current and future Bose API subdomains
while providing comprehensive TLS debugging for DNS redirection troubleshooting.
This commit is contained in:
Tobias Gesellchen
2026-02-24 21:47:20 +01:00
parent a1d0213f92
commit 5edab77209
4 changed files with 152 additions and 19 deletions
+136 -10
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
@@ -493,15 +494,23 @@ func loadConfig(c *cli.Context) serviceConfig {
func getDomains(serverURL, httpsServerURL, hostname string) []string {
domainsMap := map[string]bool{
"streaming.bose.com": true,
"updates.bose.com": true,
"stats.bose.com": true,
"bmx.bose.com": true,
"content.api.bose.io": true,
setup.TestDomain: true,
hostname: true,
"localhost": true,
"127.0.0.1": true,
// RFC-compliant wildcards for API patterns
"*.api.bose.io": true,
"*.api.bosecm.com": true,
// Core Bose domains (keep specific ones for clarity)
"streaming.bose.com": true,
"updates.bose.com": true,
"stats.bose.com": true,
"bmx.bose.com": true,
"worldwide.bose.com": true,
"music.api.bose.com": true,
"bose-prod.apigee.net": true,
"bose-test.apigee.net": true,
// Local service domains
setup.TestDomain: true,
hostname: true,
"localhost": true,
"127.0.0.1": true,
}
if u, err := url.Parse(serverURL); err == nil && u.Hostname() != "" {
@@ -789,17 +798,134 @@ func setupRouter(server *handlers.Server) *chi.Mux {
}
func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, httpsServerURL string) {
// Add custom error logging and connection state tracking
tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
log.Printf("[TLS] Certificate request for ServerName: %s", clientHello.ServerName)
// Use the default certificate selection logic
for _, cert := range tlsConfig.Certificates {
if cert.Leaf != nil {
for _, name := range cert.Leaf.DNSNames {
if matchesDomain(name, clientHello.ServerName) {
log.Printf("[TLS] ✅ Serving certificate for %s (matched %s)", clientHello.ServerName, name)
return &cert, nil
}
}
}
}
// If no specific match, return the first certificate and log it
if len(tlsConfig.Certificates) > 0 {
log.Printf("[TLS] ⚠️ No exact match for %s, using default certificate", clientHello.ServerName)
return &tlsConfig.Certificates[0], nil
}
log.Printf("[TLS] ❌ No certificate available for %s", clientHello.ServerName)
return nil, fmt.Errorf("no certificate available for %s", clientHello.ServerName)
}
httpsServer := &http.Server{
Addr: httpsAddr,
Handler: r,
TLSConfig: tlsConfig,
ErrorLog: log.Default(), // Ensure error logging is enabled
}
log.Printf("Go service starting HTTPS on %s", httpsServerURL)
go func() {
if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
listener, err := net.Listen("tcp", httpsAddr)
if err != nil {
log.Printf("[TLS] Failed to create listener: %v", err)
return
}
tlsListener := tls.NewListener(listener, tlsConfig)
// Wrap listener to log connection attempts
wrappedListener := &loggingTLSListener{
Listener: tlsListener,
}
if err := httpsServer.Serve(wrappedListener); err != nil && err != http.ErrServerClosed {
log.Printf("HTTPS server error: %v", err)
}
}()
}
// matchesDomain checks if a certificate domain (which may be a wildcard) matches a server name
func matchesDomain(certDomain, serverName string) bool {
if certDomain == serverName {
return true
}
// Handle wildcard certificates (only at the beginning of a label)
if strings.HasPrefix(certDomain, "*.") {
certBase := certDomain[2:] // Remove "*."
// For *.api.bose.io to match events.api.bose.io but not test.content.api.bose.io
// We need to ensure only one label is replaced by the wildcard
if strings.HasSuffix(serverName, "."+certBase) {
// Count dots to ensure we're not matching too many levels
serverPrefix := strings.TrimSuffix(serverName, "."+certBase)
if !strings.Contains(serverPrefix, ".") {
return true
}
}
// Also match the base domain (e.g., api.bose.io matches *.api.bose.io)
if serverName == certBase {
return true
}
}
return false
}
// loggingTLSListener wraps a TLS listener to log connection attempts and handshake failures
type loggingTLSListener struct {
net.Listener
}
func (l *loggingTLSListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
// Wrap the connection to log TLS handshake results
return &loggingTLSConn{
Conn: conn,
addr: conn.RemoteAddr(),
}, nil
}
// loggingTLSConn wraps a TLS connection to log handshake failures
type loggingTLSConn struct {
net.Conn
addr net.Addr
handshakeLogged bool
}
func (c *loggingTLSConn) Read(b []byte) (n int, err error) {
n, err = c.Conn.Read(b)
// Log TLS handshake failures on first read attempt
if !c.handshakeLogged {
c.handshakeLogged = true
if err != nil {
// Check if this looks like a TLS handshake failure
if strings.Contains(err.Error(), "tls:") ||
strings.Contains(err.Error(), "handshake") ||
strings.Contains(err.Error(), "certificate") {
log.Printf("[TLS] ❌ Handshake failed from %s: %v", c.addr, err)
}
} else if n > 0 {
log.Printf("[TLS] ✅ Successful connection from %s", c.addr)
}
}
return n, err
}
+9 -2
View File
@@ -33,16 +33,23 @@ The `soundtouch-service` now includes a built-in HTTPS listener. This simplifies
- **HTTPS Port**: Configurable via `HTTPS_PORT` environment variable (defaults to `8443`).
- **HTTPS Server URL**: Configurable via `HTTPS_SERVER_URL` (e.g., `https://mysoundtouch.local:8443`). If not set, the service attempts to guess it using the system hostname.
- **Domain Coverage**: Automatically presents a certificate for `streaming.bose.com`, `updates.bose.com`, `stats.bose.com`, `bmx.bose.com`, and `content.api.bose.io`.
- **Domain Coverage**: Automatically presents a certificate with comprehensive coverage using wildcard certificates (`*.api.bose.io`, `*.api.bosecm.com`) plus specific domains (`streaming.bose.com`, `updates.bose.com`, `stats.bose.com`, `bmx.bose.com`, `worldwide.bose.com`, `bose-prod.apigee.net`, etc.).
- **Wildcard Support**: Uses RFC-compliant wildcard certificates for automatic coverage of all API subdomains, including event analytics endpoints like `events.api.bosecm.com`, `eventsdev.api.bosecm.com`, and future API services.
- **TLS Error Logging**: Comprehensive logging of TLS handshake attempts, certificate matching, and connection failures for debugging DNS redirection issues.
- **Automatic Setup**: On first start, it generates a server certificate signed by your AfterTouch local Root CA.
#### TLS Security
#### TLS Security & Debugging
The built-in HTTPS listener is configured to use modern and secure TLS settings while maintaining compatibility with SoundTouch devices (which support up to TLS 1.2 with OpenSSL 1.0.2).
- **Minimum TLS Version**: TLS 1.2
- **Preferred Cipher Suites**:
- `ECDHE-RSA-AES128-GCM-SHA256`
- **TLS Debugging**: Detailed logging of:
- Certificate requests by domain (`[TLS] Certificate request for ServerName: events.api.bosecm.com`)
- Wildcard certificate matching (`[TLS] ✅ Serving certificate for events.api.bosecm.com (matched *.api.bosecm.com)`)
- Handshake failures (`[TLS] ❌ Handshake failed from 192.168.1.50: tls: certificate not found`)
- Successful connections (`[TLS] ✅ Successful connection from 192.168.1.50`)
- `ECDHE-RSA-AES256-GCM-SHA384`
- `ECDHE-RSA-CHACHA20-POLY1305`
- `RSA-AES128-GCM-SHA256` (Legacy support)
+2 -2
View File
@@ -103,7 +103,7 @@ func TestCertificateManager(t *testing.T) {
}
// Test certificate regeneration if domains change
newDomains := append(domains, "mac.fritz.box")
newDomains := append(domains, "foo.local")
tlsConfig2, err := cm.GetServerTLSConfig(newDomains)
if err != nil {
t.Fatalf("Failed to get updated TLS config: %v", err)
@@ -116,7 +116,7 @@ func TestCertificateManager(t *testing.T) {
cert, _ := x509.ParseCertificate(block.Bytes)
found := false
for _, d := range cert.DNSNames {
if d == "mac.fritz.box" {
if d == "foo.local" {
found = true
break
}
+5 -5
View File
@@ -52,7 +52,7 @@ func TestRecorder_Record_Structure(t *testing.T) {
{
name: "path_with_ip",
category: "self",
path: "/setup/info/192.168.178.35",
path: "/setup/info/192.168.1.100",
expected: "setup/info/{ip}",
},
{
@@ -131,7 +131,7 @@ func TestRecorder_Record_Sanitization(t *testing.T) {
req := &http.Request{
Method: "GET",
URL: &url.URL{
Path: "/info/192.168.178.35/A81B6A536A98",
Path: "/info/192.168.1.100/A81B6A536A98",
},
Header: make(http.Header),
}
@@ -331,7 +331,7 @@ func TestRecorder_EnvFile(t *testing.T) {
req := &http.Request{
Method: "GET",
URL: &url.URL{
Path: "/info/192.168.178.35",
Path: "/info/192.168.1.100",
},
Header: make(http.Header),
}
@@ -352,8 +352,8 @@ func TestRecorder_EnvFile(t *testing.T) {
t.Fatalf("Failed to unmarshal env file: %v", err)
}
if content["session"]["ip"] != "192.168.178.35" {
t.Errorf("Expected ip to be 192.168.178.35, got %s", content["session"]["ip"])
if content["session"]["ip"] != "192.168.1.100" {
t.Errorf("Expected ip to be 192.168.1.100, got %s", content["session"]["ip"])
}
}