ceremonyclient/go-libp2p/p2p/transport/webtransport/transport.go
Cassie Heart ebb31fd792 v1.4.19
2024-06-08 11:32:45 +00:00

419 lines
12 KiB
Go

package libp2pwebtransport
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"sync"
"sync/atomic"
"time"
"github.com/libp2p/go-libp2p/core/connmgr"
ic "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/pnet"
tpt "github.com/libp2p/go-libp2p/core/transport"
"github.com/libp2p/go-libp2p/p2p/security/noise"
"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
"github.com/benbjohnson/clock"
logging "github.com/ipfs/go-log/v2"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
"github.com/multiformats/go-multihash"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/webtransport-go"
)
var log = logging.Logger("webtransport")
const webtransportHTTPEndpoint = "/.well-known/libp2p-webtransport"
const errorCodeConnectionGating = 0x47415445 // GATE in ASCII
const certValidity = 14 * 24 * time.Hour
type Option func(*transport) error
func WithClock(cl clock.Clock) Option {
return func(t *transport) error {
t.clock = cl
return nil
}
}
// WithTLSClientConfig sets a custom tls.Config used for dialing.
// This option is most useful for setting a custom tls.Config.RootCAs certificate pool.
// When dialing a multiaddr that contains a /certhash component, this library will set InsecureSkipVerify and
// overwrite the VerifyPeerCertificate callback.
func WithTLSClientConfig(c *tls.Config) Option {
return func(t *transport) error {
t.tlsClientConf = c
return nil
}
}
type transport struct {
privKey ic.PrivKey
pid peer.ID
clock clock.Clock
connManager *quicreuse.ConnManager
rcmgr network.ResourceManager
gater connmgr.ConnectionGater
listenOnce sync.Once
listenOnceErr error
certManager *certManager
hasCertManager atomic.Bool // set to true once the certManager is initialized
staticTLSConf *tls.Config
tlsClientConf *tls.Config
noise *noise.Transport
connMx sync.Mutex
conns map[quic.ConnectionTracingID]*conn // using quic-go's ConnectionTracingKey as map key
}
var _ tpt.Transport = &transport{}
var _ tpt.Resolver = &transport{}
var _ io.Closer = &transport{}
func New(key ic.PrivKey, psk pnet.PSK, connManager *quicreuse.ConnManager, gater connmgr.ConnectionGater, rcmgr network.ResourceManager, opts ...Option) (tpt.Transport, error) {
if len(psk) > 0 {
log.Error("WebTransport doesn't support private networks yet.")
return nil, errors.New("WebTransport doesn't support private networks yet")
}
if rcmgr == nil {
rcmgr = &network.NullResourceManager{}
}
id, err := peer.IDFromPrivateKey(key)
if err != nil {
return nil, err
}
t := &transport{
pid: id,
privKey: key,
rcmgr: rcmgr,
gater: gater,
clock: clock.New(),
connManager: connManager,
conns: map[quic.ConnectionTracingID]*conn{},
}
for _, opt := range opts {
if err := opt(t); err != nil {
return nil, err
}
}
n, err := noise.New(noise.ID, key, nil)
if err != nil {
return nil, err
}
t.noise = n
return t, nil
}
func (t *transport) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (tpt.CapableConn, error) {
scope, err := t.rcmgr.OpenConnection(network.DirOutbound, false, raddr)
if err != nil {
log.Debugw("resource manager blocked outgoing connection", "peer", p, "addr", raddr, "error", err)
return nil, err
}
c, err := t.dialWithScope(ctx, raddr, p, scope)
if err != nil {
scope.Done()
return nil, err
}
return c, nil
}
func (t *transport) dialWithScope(ctx context.Context, raddr ma.Multiaddr, p peer.ID, scope network.ConnManagementScope) (tpt.CapableConn, error) {
_, addr, err := manet.DialArgs(raddr)
if err != nil {
return nil, err
}
url := fmt.Sprintf("https://%s%s?type=noise", addr, webtransportHTTPEndpoint)
certHashes, err := extractCertHashes(raddr)
if err != nil {
return nil, err
}
if len(certHashes) == 0 {
return nil, errors.New("can't dial webtransport without certhashes")
}
sni, _ := extractSNI(raddr)
if err := scope.SetPeer(p); err != nil {
log.Debugw("resource manager blocked outgoing connection for peer", "peer", p, "addr", raddr, "error", err)
return nil, err
}
maddr, _ := ma.SplitFunc(raddr, func(c ma.Component) bool { return c.Protocol().Code == ma.P_WEBTRANSPORT })
sess, err := t.dial(ctx, maddr, url, sni, certHashes)
if err != nil {
return nil, err
}
sconn, err := t.upgrade(ctx, sess, p, certHashes)
if err != nil {
sess.CloseWithError(1, "")
return nil, err
}
if t.gater != nil && !t.gater.InterceptSecured(network.DirOutbound, p, sconn) {
sess.CloseWithError(errorCodeConnectionGating, "")
return nil, fmt.Errorf("secured connection gated")
}
conn := newConn(t, sess, sconn, scope)
t.addConn(sess, conn)
return conn, nil
}
func (t *transport) dial(ctx context.Context, addr ma.Multiaddr, url, sni string, certHashes []multihash.DecodedMultihash) (*webtransport.Session, error) {
var tlsConf *tls.Config
if t.tlsClientConf != nil {
tlsConf = t.tlsClientConf.Clone()
} else {
tlsConf = &tls.Config{}
}
tlsConf.NextProtos = append(tlsConf.NextProtos, http3.NextProtoH3)
if sni != "" {
tlsConf.ServerName = sni
}
if len(certHashes) > 0 {
// This is not insecure. We verify the certificate ourselves.
// See https://www.w3.org/TR/webtransport/#certificate-hashes.
tlsConf.InsecureSkipVerify = true
tlsConf.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
return verifyRawCerts(rawCerts, certHashes)
}
}
conn, err := t.connManager.DialQUIC(ctx, addr, tlsConf, t.allowWindowIncrease)
if err != nil {
return nil, err
}
dialer := webtransport.Dialer{
DialAddr: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
return conn.(quic.EarlyConnection), nil
},
QUICConfig: t.connManager.ClientConfig().Clone(),
}
rsp, sess, err := dialer.Dial(ctx, url, nil)
if err != nil {
return nil, err
}
if rsp.StatusCode < 200 || rsp.StatusCode > 299 {
return nil, fmt.Errorf("invalid response status code: %d", rsp.StatusCode)
}
return sess, err
}
func (t *transport) upgrade(ctx context.Context, sess *webtransport.Session, p peer.ID, certHashes []multihash.DecodedMultihash) (*connSecurityMultiaddrs, error) {
local, err := toWebtransportMultiaddr(sess.LocalAddr())
if err != nil {
return nil, fmt.Errorf("error determining local addr: %w", err)
}
remote, err := toWebtransportMultiaddr(sess.RemoteAddr())
if err != nil {
return nil, fmt.Errorf("error determining remote addr: %w", err)
}
str, err := sess.OpenStreamSync(ctx)
if err != nil {
return nil, err
}
defer str.Close()
// Now run a Noise handshake (using early data) and get all the certificate hashes from the server.
// We will verify that the certhashes we used to dial is a subset of the certhashes we received from the server.
var verified bool
n, err := t.noise.WithSessionOptions(noise.EarlyData(newEarlyDataReceiver(func(b *pb.NoiseExtensions) error {
decodedCertHashes, err := decodeCertHashesFromProtobuf(b.WebtransportCerthashes)
if err != nil {
return err
}
for _, sent := range certHashes {
var found bool
for _, rcvd := range decodedCertHashes {
if sent.Code == rcvd.Code && bytes.Equal(sent.Digest, rcvd.Digest) {
found = true
break
}
}
if !found {
return fmt.Errorf("missing cert hash: %v", sent)
}
}
verified = true
return nil
}), nil))
if err != nil {
return nil, fmt.Errorf("failed to create Noise transport: %w", err)
}
c, err := n.SecureOutbound(ctx, &webtransportStream{Stream: str, wsess: sess}, p)
if err != nil {
return nil, err
}
defer c.Close()
// The Noise handshake _should_ guarantee that our verification callback is called.
// Double-check just in case.
if !verified {
return nil, errors.New("didn't verify")
}
return &connSecurityMultiaddrs{
ConnSecurity: c,
ConnMultiaddrs: &connMultiaddrs{local: local, remote: remote},
}, nil
}
func decodeCertHashesFromProtobuf(b [][]byte) ([]multihash.DecodedMultihash, error) {
hashes := make([]multihash.DecodedMultihash, 0, len(b))
for _, h := range b {
dh, err := multihash.Decode(h)
if err != nil {
return nil, fmt.Errorf("failed to decode hash: %w", err)
}
hashes = append(hashes, *dh)
}
return hashes, nil
}
func (t *transport) CanDial(addr ma.Multiaddr) bool {
ok, _ := IsWebtransportMultiaddr(addr)
return ok
}
func (t *transport) Listen(laddr ma.Multiaddr) (tpt.Listener, error) {
isWebTransport, certhashCount := IsWebtransportMultiaddr(laddr)
if !isWebTransport {
return nil, fmt.Errorf("cannot listen on non-WebTransport addr: %s", laddr)
}
if certhashCount > 0 {
return nil, fmt.Errorf("cannot listen on a specific certhash non-WebTransport addr: %s", laddr)
}
if t.staticTLSConf == nil {
t.listenOnce.Do(func() {
t.certManager, t.listenOnceErr = newCertManager(t.privKey, t.clock)
t.hasCertManager.Store(true)
})
if t.listenOnceErr != nil {
return nil, t.listenOnceErr
}
} else {
return nil, errors.New("static TLS config not supported on WebTransport")
}
tlsConf := t.staticTLSConf.Clone()
if tlsConf == nil {
tlsConf = &tls.Config{GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
return t.certManager.GetConfig(), nil
}}
}
tlsConf.NextProtos = append(tlsConf.NextProtos, http3.NextProtoH3)
ln, err := t.connManager.ListenQUIC(laddr, tlsConf, t.allowWindowIncrease)
if err != nil {
return nil, err
}
return newListener(ln, t, t.staticTLSConf != nil)
}
func (t *transport) Protocols() []int {
return []int{ma.P_WEBTRANSPORT}
}
func (t *transport) Proxy() bool {
return false
}
func (t *transport) Close() error {
t.listenOnce.Do(func() {})
if t.certManager != nil {
return t.certManager.Close()
}
return nil
}
func (t *transport) allowWindowIncrease(conn quic.Connection, size uint64) bool {
t.connMx.Lock()
defer t.connMx.Unlock()
c, ok := t.conns[conn.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)]
if !ok {
return false
}
return c.allowWindowIncrease(size)
}
func (t *transport) addConn(sess *webtransport.Session, c *conn) {
t.connMx.Lock()
t.conns[sess.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)] = c
t.connMx.Unlock()
}
func (t *transport) removeConn(sess *webtransport.Session) {
t.connMx.Lock()
delete(t.conns, sess.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID))
t.connMx.Unlock()
}
// extractSNI returns what the SNI should be for the given maddr. If there is an
// SNI component in the multiaddr, then it will be returned and
// foundSniComponent will be true. If there's no SNI component, but there is a
// DNS-like component, then that will be returned for the sni and
// foundSniComponent will be false (since we didn't find an actual sni component).
func extractSNI(maddr ma.Multiaddr) (sni string, foundSniComponent bool) {
ma.ForEach(maddr, func(c ma.Component) bool {
switch c.Protocol().Code {
case ma.P_SNI:
sni = c.Value()
foundSniComponent = true
return false
case ma.P_DNS, ma.P_DNS4, ma.P_DNS6, ma.P_DNSADDR:
sni = c.Value()
// Keep going in case we find an `sni` component
return true
}
return true
})
return sni, foundSniComponent
}
// Resolve implements transport.Resolver
func (t *transport) Resolve(_ context.Context, maddr ma.Multiaddr) ([]ma.Multiaddr, error) {
sni, foundSniComponent := extractSNI(maddr)
if foundSniComponent || sni == "" {
// The multiaddr already had an sni field, we can keep using it. Or we don't have any sni like thing
return []ma.Multiaddr{maddr}, nil
}
beforeQuicMA, afterIncludingQuicMA := ma.SplitFunc(maddr, func(c ma.Component) bool {
return c.Protocol().Code == ma.P_QUIC_V1
})
quicComponent, afterQuicMA := ma.SplitFirst(afterIncludingQuicMA)
sniComponent, err := ma.NewComponent(ma.ProtocolWithCode(ma.P_SNI).Name, sni)
if err != nil {
return nil, err
}
return []ma.Multiaddr{beforeQuicMA.Encapsulate(quicComponent).Encapsulate(sniComponent).Encapsulate(afterQuicMA)}, nil
}
// AddCertHashes adds the current certificate hashes to a multiaddress.
// If called before Listen, it's a no-op.
func (t *transport) AddCertHashes(m ma.Multiaddr) (ma.Multiaddr, bool) {
if !t.hasCertManager.Load() {
return m, false
}
return m.Encapsulate(t.certManager.AddrComponent()), true
}