Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move go-libp2p-webtransport to p2p/transport/webtransport #1737

Merged
merged 54 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
9c2be3c
initial commit
marten-seemann Apr 13, 2022
528fa96
use a context to control listener shutdown
marten-seemann Apr 16, 2022
c7ebc6d
run a Noise handshake on the first stream
marten-seemann Apr 16, 2022
1ea3360
generate a single certificate, not a chain
marten-seemann Apr 16, 2022
a077c89
implement a basic certificate manager
marten-seemann Apr 17, 2022
268725c
check that addr contains at least one certhash in CanDial
marten-seemann Apr 18, 2022
2d295b5
fix copying of http.Server mutex in listener constructor
marten-seemann Apr 18, 2022
59fc6ef
fix staticcheck
marten-seemann Apr 18, 2022
85384b7
fix flaky TestCertRenewal test on CI
marten-seemann Apr 18, 2022
77cdde6
check validity of listen addresses
marten-seemann Apr 18, 2022
ee0e2d0
initialize a certiticate manager and pass it to the listeners
marten-seemann Apr 18, 2022
ae21c6c
implement a Close method for the transport
marten-seemann Apr 19, 2022
323960e
chore: update webtransport-go
marten-seemann May 21, 2022
0af48f5
run a Noise handshake on the first stream to verify peer identities
marten-seemann May 22, 2022
52ae9b7
simplify the listener.Accept logic
marten-seemann May 22, 2022
91e9730
don't close the HTTP response body when dialing
marten-seemann May 22, 2022
d8209e3
add more tests for failed handshakes
marten-seemann May 22, 2022
47be412
return local and remote multiaddrs from conn
marten-seemann May 22, 2022
a0eec0f
update webtransport-go, rename webtransport.Conn to Session
marten-seemann Jul 2, 2022
d155f11
tighten multiaddr conversion logic
marten-seemann Jul 9, 2022
106cbc3
return the listener's HTTP handler as soon as the handshake completes
marten-seemann Jul 9, 2022
ed5a2f5
use the resource manager when dialing
marten-seemann Jul 9, 2022
468fd51
move the HTTP handler to a separate method on the listener
marten-seemann Jul 9, 2022
2110e68
add a function to convert a IP:port string to a WebTransport multiaddr
marten-seemann Jul 9, 2022
f55a4ff
use the resource manager when accepting connections
marten-seemann Jul 9, 2022
3b13c83
move extraction of certificate hashes to a separate function
marten-seemann Jul 9, 2022
c7149b3
pass the connection scope to the connection (#11)
marten-seemann Jul 9, 2022
69341f6
properly implement conn.IsClosed (#13)
marten-seemann Jul 9, 2022
ecc1eff
refactor the conn constructor
marten-seemann Jul 9, 2022
2c6fc83
implement connection gating for dials
marten-seemann Jul 9, 2022
a508e94
implement InterceptAccept connection gating
marten-seemann Jul 9, 2022
7e8ca3a
implement InterceptSecured for accepted connections
marten-seemann Jul 9, 2022
851e6ba
verify the hash of the server's certificate (#16)
marten-seemann Jul 10, 2022
f0dbd3e
fix flaky resource manager test (#19)
marten-seemann Jul 10, 2022
d626e80
move connection interface to conn.go
marten-seemann Jul 10, 2022
ebcb513
use a mock clock in cert manager tests (#20)
marten-seemann Jul 10, 2022
9f2e830
remove member variable for certificate validity from cert manager
marten-seemann Jul 10, 2022
ff5aa30
simplify certificate generation
marten-seemann Jul 12, 2022
2823159
optimize expiry periods of certificates (#21)
marten-seemann Jul 16, 2022
d74921d
make it possible to use a custom tls.Config for listening and dialing…
marten-seemann Jul 16, 2022
60a4071
chore: update CI to Go 1.18 / 1.19, update webtransport-go to v0.1.0
marten-seemann Aug 29, 2022
4ce4e4f
only use positive numbers for x509.Certificate serial numbers
marten-seemann Sep 3, 2022
3521b4f
chore: update go-multiaddr to v0.6.0
marten-seemann Sep 3, 2022
97e739f
update to the current master of go-libp2p (#23)
marten-seemann Sep 3, 2022
0295df0
remove Hello World HTTP endpoint
marten-seemann Sep 7, 2022
c783884
add a link to the certificate hashes section of the w3c WebTransport …
marten-seemann Sep 7, 2022
ef657d6
make addrComponentForCert a pure function
marten-seemann Sep 7, 2022
d2b47dd
remove the unneeded connSecurityMultiaddrs interface
marten-seemann Sep 7, 2022
86a058d
close the scope when the connection gater intercepts
marten-seemann Sep 7, 2022
f49dd4f
clarify comment about certificate hash verification
marten-seemann Sep 7, 2022
7714b04
drop incoming sessions when the accept queue fills up (#24)
marten-seemann Sep 7, 2022
98ff0c6
fix CanDial for addresses that don't contain any certificate hashes (…
marten-seemann Sep 7, 2022
48341e4
move go-libp2p-webtransport here
marten-seemann Sep 7, 2022
73b3d56
webtransport: adjust import paths
marten-seemann Sep 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ require (
github.com/libp2p/zeroconf/v2 v2.2.0
github.com/lucas-clemente/quic-go v0.29.0
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd
github.com/marten-seemann/webtransport-go v0.1.0
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b
github.com/minio/sha256-simd v1.0.0
github.com/mr-tron/base58 v1.2.0
github.com/multiformats/go-base32 v0.0.4
github.com/multiformats/go-multiaddr v0.6.0
github.com/multiformats/go-multiaddr-dns v0.3.1
github.com/multiformats/go-multiaddr-fmt v0.1.0
github.com/multiformats/go-multibase v0.1.1
github.com/multiformats/go-multicodec v0.5.0
github.com/multiformats/go-multihash v0.2.1
github.com/multiformats/go-multistream v0.3.3
Expand Down Expand Up @@ -81,6 +83,7 @@ require (
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
github.com/koron/go-ssdp v0.0.3 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
Expand All @@ -89,7 +92,6 @@ require (
github.com/miekg/dns v1.1.50 // indirect
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
github.com/multiformats/go-base36 v0.1.0 // indirect
github.com/multiformats/go-multibase v0.1.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/opencontainers/runtime-spec v1.0.2 // indirect
Expand All @@ -107,6 +109,7 @@ require (
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,16 @@ github.com/lucas-clemente/quic-go v0.29.0/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
github.com/marten-seemann/webtransport-go v0.1.0 h1:tKknMOI+9fmMTPRJv3Fz7XpDq67Tg8Va8B1QEE+qaZw=
github.com/marten-seemann/webtransport-go v0.1.0/go.mod h1:CACJOFSohYRmnqHWHWZ2rpTr9MfA0pNqOSkpqb6J/+w=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
Expand Down Expand Up @@ -393,6 +397,7 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
Expand Down Expand Up @@ -695,6 +700,7 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
168 changes: 168 additions & 0 deletions p2p/transport/webtransport/cert_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package libp2pwebtransport

import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"fmt"
"sync"
"time"

"github.com/benbjohnson/clock"
ma "github.com/multiformats/go-multiaddr"
"github.com/multiformats/go-multihash"
)

// Allow for a bit of clock skew.
// When we generate a certificate, the NotBefore time is set to clockSkewAllowance before the current time.
// Similarly, we stop using a certificate one clockSkewAllowance before its expiry time.
const clockSkewAllowance = time.Hour

type certConfig struct {
tlsConf *tls.Config
sha256 [32]byte // cached from the tlsConf
}

func (c *certConfig) Start() time.Time { return c.tlsConf.Certificates[0].Leaf.NotBefore }
func (c *certConfig) End() time.Time { return c.tlsConf.Certificates[0].Leaf.NotAfter }

func newCertConfig(start, end time.Time) (*certConfig, error) {
conf, err := getTLSConf(start, end)
if err != nil {
return nil, err
}
return &certConfig{
tlsConf: conf,
sha256: sha256.Sum256(conf.Certificates[0].Leaf.Raw),
}, nil
}

// Certificate renewal logic:
// 1. On startup, we generate one cert that is valid from now (-1h, to allow for clock skew), and another
// cert that is valid from the expiry date of the first certificate (again, with allowance for clock skew).
// 2. Once we reach 1h before expiry of the first certificate, we switch over to the second certificate.
// At the same time, we stop advertising the certhash of the first cert and generate the next cert.
type certManager struct {
clock clock.Clock
ctx context.Context
ctxCancel context.CancelFunc
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is an anti-pattern. https://go.dev/blog/context-and-structs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this is also the pattern we've been using all across go-libp2p, in every service that spawns Go routines. It's super convenient since you can call the CancelFunc multiple times (which you can't do if you do the naive implementation with a closeChan chan struct{}, as you can only close a channel once).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I know we do it a lot. Just worth pointing out that I don’t think it’s idiomatic go, and maybe we should stop adding new code like this.

Not a blocking change of course

refCount sync.WaitGroup

mx sync.RWMutex
lastConfig *certConfig // initially nil
currentConfig *certConfig
nextConfig *certConfig // nil until we have passed half the certValidity of the current config
addrComp ma.Multiaddr
}

func newCertManager(clock clock.Clock) (*certManager, error) {
m := &certManager{clock: clock}
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
if err := m.init(); err != nil {
return nil, err
}

m.background()
return m, nil
}

func (m *certManager) init() error {
start := m.clock.Now().Add(-clockSkewAllowance)
var err error
m.nextConfig, err = newCertConfig(start, start.Add(certValidity))
if err != nil {
return err
}
return m.rollConfig()
}

func (m *certManager) rollConfig() error {
// We stop using the current certificate clockSkewAllowance before its expiry time.
// At this point, the next certificate needs to be valid for one clockSkewAllowance.
nextStart := m.nextConfig.End().Add(-2 * clockSkewAllowance)
c, err := newCertConfig(nextStart, nextStart.Add(certValidity))
if err != nil {
return err
}
m.lastConfig = m.currentConfig
m.currentConfig = m.nextConfig
m.nextConfig = c
return m.cacheAddrComponent()
}

func (m *certManager) background() {
d := m.currentConfig.End().Add(-clockSkewAllowance).Sub(m.clock.Now())
log.Debugw("setting timer", "duration", d.String())
t := m.clock.Timer(d)
m.refCount.Add(1)

go func() {
defer m.refCount.Done()
defer t.Stop()

for {
select {
case <-m.ctx.Done():
return
case now := <-t.C:
m.mx.Lock()
if err := m.rollConfig(); err != nil {
log.Errorw("rolling config failed", "error", err)
}
d := m.currentConfig.End().Add(-clockSkewAllowance).Sub(now)
log.Debugw("rolling certificates", "next", d.String())
t.Reset(d)
m.mx.Unlock()
}
}
}()
}

func (m *certManager) GetConfig() *tls.Config {
m.mx.RLock()
defer m.mx.RUnlock()
return m.currentConfig.tlsConf
}

func (m *certManager) AddrComponent() ma.Multiaddr {
m.mx.RLock()
defer m.mx.RUnlock()
return m.addrComp
}

func (m *certManager) Verify(hashes []multihash.DecodedMultihash) error {
for _, h := range hashes {
if h.Code != multihash.SHA2_256 {
return fmt.Errorf("expected SHA256 hash, got %d", h.Code)
}
if !bytes.Equal(h.Digest, m.currentConfig.sha256[:]) &&
(m.nextConfig == nil || !bytes.Equal(h.Digest, m.nextConfig.sha256[:])) &&
(m.lastConfig == nil || !bytes.Equal(h.Digest, m.lastConfig.sha256[:])) {
return fmt.Errorf("found unexpected hash: %+x", h.Digest)
}
}
return nil
}

func (m *certManager) cacheAddrComponent() error {
addr, err := addrComponentForCert(m.currentConfig.sha256[:])
if err != nil {
return err
}
if m.nextConfig != nil {
comp, err := addrComponentForCert(m.nextConfig.sha256[:])
if err != nil {
return err
}
addr = addr.Encapsulate(comp)
}
m.addrComp = addr
return nil
}

func (m *certManager) Close() error {
m.ctxCancel()
m.refCount.Wait()
return nil
}
102 changes: 102 additions & 0 deletions p2p/transport/webtransport/cert_manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package libp2pwebtransport

import (
"crypto/sha256"
"crypto/tls"
"testing"
"time"

"github.com/benbjohnson/clock"
ma "github.com/multiformats/go-multiaddr"
"github.com/multiformats/go-multibase"
"github.com/multiformats/go-multihash"
"github.com/stretchr/testify/require"
)

func certificateHashFromTLSConfig(c *tls.Config) [32]byte {
return sha256.Sum256(c.Certificates[0].Certificate[0])
}

func splitMultiaddr(addr ma.Multiaddr) []ma.Component {
var components []ma.Component
ma.ForEach(addr, func(c ma.Component) bool {
components = append(components, c)
return true
})
return components
}

func certHashFromComponent(t *testing.T, comp ma.Component) []byte {
t.Helper()
_, data, err := multibase.Decode(comp.Value())
require.NoError(t, err)
mh, err := multihash.Decode(data)
require.NoError(t, err)
require.Equal(t, uint64(multihash.SHA2_256), mh.Code)
return mh.Digest
}

func TestInitialCert(t *testing.T) {
cl := clock.NewMock()
cl.Add(1234567 * time.Hour)
m, err := newCertManager(cl)
require.NoError(t, err)
defer m.Close()

conf := m.GetConfig()
require.Len(t, conf.Certificates, 1)
cert := conf.Certificates[0]
require.Equal(t, cl.Now().Add(-clockSkewAllowance).UTC(), cert.Leaf.NotBefore)
require.Equal(t, cert.Leaf.NotBefore.Add(certValidity), cert.Leaf.NotAfter)
addr := m.AddrComponent()
components := splitMultiaddr(addr)
require.Len(t, components, 2)
require.Equal(t, ma.P_CERTHASH, components[0].Protocol().Code)
hash := certificateHashFromTLSConfig(conf)
require.Equal(t, hash[:], certHashFromComponent(t, components[0]))
require.Equal(t, ma.P_CERTHASH, components[1].Protocol().Code)
}

func TestCertRenewal(t *testing.T) {
cl := clock.NewMock()
m, err := newCertManager(cl)
require.NoError(t, err)
defer m.Close()

firstConf := m.GetConfig()
first := splitMultiaddr(m.AddrComponent())
require.Len(t, first, 2)
require.NotEqual(t, first[0].Value(), first[1].Value(), "the hashes should differ")
// wait for a new certificate to be generated
cl.Add(certValidity - 2*clockSkewAllowance - time.Second)
require.Never(t, func() bool {
for i, c := range splitMultiaddr(m.AddrComponent()) {
if c.Value() != first[i].Value() {
return true
}
}
return false
}, 100*time.Millisecond, 10*time.Millisecond)
cl.Add(2 * time.Second)
require.Eventually(t, func() bool { return m.GetConfig() != firstConf }, 200*time.Millisecond, 10*time.Millisecond)
secondConf := m.GetConfig()

second := splitMultiaddr(m.AddrComponent())
require.Len(t, second, 2)
for _, c := range second {
require.Equal(t, ma.P_CERTHASH, c.Protocol().Code)
}
// check that the 2nd certificate from the beginning was rolled over to be the 1st certificate
require.Equal(t, first[1].Value(), second[0].Value())
require.NotEqual(t, first[0].Value(), second[1].Value())

cl.Add(certValidity - 2*clockSkewAllowance + time.Second)
require.Eventually(t, func() bool { return m.GetConfig() != secondConf }, 200*time.Millisecond, 10*time.Millisecond)
third := splitMultiaddr(m.AddrComponent())
require.Len(t, third, 2)
for _, c := range third {
require.Equal(t, ma.P_CERTHASH, c.Protocol().Code)
}
// check that the 2nd certificate from the beginning was rolled over to be the 1st certificate
require.Equal(t, second[1].Value(), third[0].Value())
}
Loading