session: fix mailbox TLS ALPN regression

Starting in grpc-go v1.67.0, clients and servers reject TLS handshakes when
ALPN is not negotiated. This changed the default value of
GRPC_ENFORCE_ALPN_ENABLED from false to true.

The default flip is in https://github.com/grpc/grpc-go/pull/7535

Our mailbox transport handshake can reach endpoints that currently do not
negotiate ALPN, so LNC session setup started failing and the lnc_auth flow
timed out, with malformed header/content-type errors showing up later on the
stream path.

This adds mailbox-specific TLS transport credentials that allow a missing
negotiated ALPN value for mailbox links. The mailbox server path and the
integration-test mailbox clients now use these credentials, so session
establishment works again.
This commit is contained in:
Boris Nagaev 2026-03-17 16:52:26 -05:00
parent 369f5e050b
commit 1629e3106a
No known key found for this signature in database
4 changed files with 127 additions and 4 deletions

View file

@ -2673,7 +2673,7 @@ func connectMailboxWithRemoteKey(ctx context.Context,
transportConn, err := mailbox.NewGrpcClient(
ctx, mailboxServerAddr, connData,
grpc.WithTransportCredentials(
credentials.NewTLS(&tls.Config{}),
session.NewMailboxTLSCredentials(&tls.Config{}),
),
)
if err != nil {

View file

@ -20,6 +20,7 @@ import (
"github.com/lightninglabs/faraday/frdrpc"
"github.com/lightninglabs/lightning-node-connect/mailbox"
"github.com/lightninglabs/lightning-terminal/litrpc"
"github.com/lightninglabs/lightning-terminal/session"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/pool/poolrpc"
"github.com/lightninglabs/taproot-assets/taprpc"
@ -1359,7 +1360,7 @@ func connectMailboxWithPairingPhrase(ctx context.Context,
transportConn, err := mailbox.NewGrpcClient(
ctx, mailboxServerAddr, connData,
grpc.WithTransportCredentials(
credentials.NewTLS(&tls.Config{}),
session.NewMailboxTLSCredentials(&tls.Config{}),
),
)
if err != nil {

View file

@ -12,7 +12,6 @@ import (
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/keychain"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/keepalive"
)
@ -61,7 +60,9 @@ func (m *mailboxSession) start(session *Session,
// Start the mailbox gRPC server.
mailboxServer, err := mailbox.NewServer(
session.ServerAddr, keys, onNewStatus,
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
grpc.WithTransportCredentials(
NewMailboxTLSCredentials(tlsConfig),
),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 2 * time.Minute,
}),

121
session/tls_credentials.go Normal file
View file

@ -0,0 +1,121 @@
package session
import (
"context"
"crypto/tls"
"net"
"google.golang.org/grpc/credentials"
)
const http2Proto = "h2"
// NewMailboxTLSCredentials creates transport credentials for mailbox
// connections. Unlike grpc's default TLS credentials, this allows endpoints
// that don't negotiate ALPN yet. This is needed as a workaround for grpc-go
// v1.67.0 ALPN enforcement added by https://github.com/grpc/grpc-go/pull/7535
//
// TODO: remove this file when all target mailbox endpoints support ALPN.
func NewMailboxTLSCredentials(
config *tls.Config) credentials.TransportCredentials {
tlsConfig := &tls.Config{}
if config != nil {
tlsConfig = config.Clone()
}
return &mailboxTLSCreds{config: tlsConfig}
}
// mailboxTLSCreds is a mailbox-specific TransportCredentials implementation
// that keeps TLS enabled but tolerates peers that do not negotiate ALPN.
type mailboxTLSCreds struct {
config *tls.Config
}
// ClientHandshake performs the client side TLS handshake for mailbox links
// while allowing an empty negotiated ALPN value from the remote endpoint.
func (c *mailboxTLSCreds) ClientHandshake(ctx context.Context, authority string,
rawConn net.Conn) (_ net.Conn, _ credentials.AuthInfo, err error) {
cfg := c.config.Clone()
if cfg.ServerName == "" {
serverName, _, err := net.SplitHostPort(authority)
if err != nil {
serverName = authority
}
cfg.ServerName = serverName
}
cfg.NextProtos = appendH2(cfg.NextProtos)
conn := tls.Client(rawConn, cfg)
if err := conn.HandshakeContext(ctx); err != nil {
_ = conn.Close()
return nil, nil, err
}
return conn, tlsAuthInfo(conn.ConnectionState()), nil
}
// ServerHandshake performs the server side TLS handshake for mailbox links
// while allowing an empty negotiated ALPN value from the peer.
func (c *mailboxTLSCreds) ServerHandshake(rawConn net.Conn) (
net.Conn, credentials.AuthInfo, error) {
cfg := c.config.Clone()
cfg.NextProtos = appendH2(cfg.NextProtos)
conn := tls.Server(rawConn, cfg)
if err := conn.Handshake(); err != nil {
_ = conn.Close()
return nil, nil, err
}
return conn, tlsAuthInfo(conn.ConnectionState()), nil
}
// Info returns protocol metadata for these transport credentials.
func (c *mailboxTLSCreds) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{
SecurityProtocol: "tls",
SecurityVersion: "1.2",
ServerName: c.config.ServerName,
}
}
// Clone returns a copy of these transport credentials.
func (c *mailboxTLSCreds) Clone() credentials.TransportCredentials {
return &mailboxTLSCreds{config: c.config.Clone()}
}
// OverrideServerName overrides the TLS server name used by the client side
// handshake.
func (c *mailboxTLSCreds) OverrideServerName(serverNameOverride string) error {
c.config.ServerName = serverNameOverride
return nil
}
// tlsAuthInfo builds credentials.AuthInfo from TLS connection state.
func tlsAuthInfo(state tls.ConnectionState) credentials.TLSInfo {
return credentials.TLSInfo{
State: state,
CommonAuthInfo: credentials.CommonAuthInfo{
SecurityLevel: credentials.PrivacyAndIntegrity,
},
}
}
// appendH2 ensures "h2" is present in the ALPN protocol list.
func appendH2(nextProtos []string) []string {
for _, proto := range nextProtos {
if proto == http2Proto {
return nextProtos
}
}
return append(nextProtos, http2Proto)
}