From 1ecfe59ba2dac16269155e45d398eb27c45044ee Mon Sep 17 00:00:00 2001 From: Chris Schinnerl <3903476+ChrisSchinnerl@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:40:52 +0100 Subject: [PATCH 1/4] Enable StreamResetPartialDelivery in QUIC server --- rhp/v4/quic/quic.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rhp/v4/quic/quic.go b/rhp/v4/quic/quic.go index dd4d42f..e501461 100644 --- a/rhp/v4/quic/quic.go +++ b/rhp/v4/quic/quic.go @@ -224,10 +224,11 @@ func Listen(conn net.PacketConn, certs CertManager) (*quic.Listener, error) { GetCertificate: certs.GetCertificate, NextProtos: []string{TLSNextProtoRHP4, http3.NextProtoH3}, }, &quic.Config{ - EnableDatagrams: true, - KeepAlivePeriod: 30 * time.Second, - MaxIdleTimeout: 30 * time.Minute, - MaxIncomingStreams: maxIncomingStreams, + EnableDatagrams: true, + KeepAlivePeriod: 30 * time.Second, + MaxIdleTimeout: 30 * time.Minute, + MaxIncomingStreams: maxIncomingStreams, + EnableStreamResetPartialDelivery: true, }) } From 0bf8d9df04b26e388a3716077b4bb99155c0d97e Mon Sep 17 00:00:00 2001 From: Chris Schinnerl <3903476+ChrisSchinnerl@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:41:17 +0100 Subject: [PATCH 2/4] changeset --- ...setpartialdelivery_when_listening_for_quic_connections.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/enable_streamresetpartialdelivery_when_listening_for_quic_connections.md diff --git a/.changeset/enable_streamresetpartialdelivery_when_listening_for_quic_connections.md b/.changeset/enable_streamresetpartialdelivery_when_listening_for_quic_connections.md new file mode 100644 index 0000000..2bfc2cf --- /dev/null +++ b/.changeset/enable_streamresetpartialdelivery_when_listening_for_quic_connections.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +# Enable StreamResetPartialDelivery when listening for QUIC connections. From f3338478b19a14fd88c0a0eb67f477a87f36d0d1 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl <3903476+ChrisSchinnerl@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:48:46 +0100 Subject: [PATCH 3/4] logging --- rhp/v4/quic/quic.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/rhp/v4/quic/quic.go b/rhp/v4/quic/quic.go index e501461..05a5bba 100644 --- a/rhp/v4/quic/quic.go +++ b/rhp/v4/quic/quic.go @@ -130,10 +130,11 @@ func Dial(ctx context.Context, addr string, peerKey types.PublicKey, opts ...Cli NextProtos: []string{TLSNextProtoRHP4}, } qc := &quic.Config{ - EnableDatagrams: true, - HandshakeIdleTimeout: 15 * time.Second, - KeepAlivePeriod: 30 * time.Second, - MaxIdleTimeout: 30 * time.Minute, + EnableDatagrams: true, + HandshakeIdleTimeout: 15 * time.Second, + KeepAlivePeriod: 30 * time.Second, + MaxIdleTimeout: 30 * time.Minute, + EnableStreamResetPartialDelivery: true, } cc := &clientConfig{ streamMiddleware: func(nc net.Conn) net.Conn { return nc }, @@ -312,13 +313,15 @@ func Serve(l *quic.Listener, s *rhp4.Server, opts ...ServeOption) { case http3.NextProtoH3: // webtransport go func() { defer conn.CloseWithError(0, "") - wts.ServeQUICConn(conn) + if err := wts.ServeQUICConn(conn); err != nil { + log.Debug("failed to serve webtransport connection", zap.Error(err)) + } }() case TLSNextProtoRHP4: // quic go func() { defer conn.CloseWithError(0, "") if err := s.Serve(&transport{qc: conn, streamMiddleware: o.streamMiddleware}, log); err != nil { - log.Debug("failed to serve connection", zap.Error(err)) + log.Debug("failed to serve quic connection", zap.Error(err)) } }() default: From 95df8ef598e06336e6133bfda3bd1b0a5b540326 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl <3903476+ChrisSchinnerl@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:55:58 +0100 Subject: [PATCH 4/4] configure server and add test --- rhp/v4/quic/quic.go | 3 ++ rhp/v4/rpc_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/rhp/v4/quic/quic.go b/rhp/v4/quic/quic.go index 05a5bba..173c8ad 100644 --- a/rhp/v4/quic/quic.go +++ b/rhp/v4/quic/quic.go @@ -281,6 +281,9 @@ func Serve(l *quic.Listener, s *rhp4.Server, opts ...ServeOption) { } defer wts.Close() + // configure the HTTP/3 server for WebTransport (enables extended CONNECT, etc.) + webtransport.ConfigureHTTP3Server(wts.H3) + mux.HandleFunc("/sia/rhp/v4", func(w http.ResponseWriter, r *http.Request) { sess, err := wts.Upgrade(w, r) if err != nil { diff --git a/rhp/v4/rpc_test.go b/rhp/v4/rpc_test.go index b586deb..e4b0d0a 100644 --- a/rhp/v4/rpc_test.go +++ b/rhp/v4/rpc_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/tls" + "fmt" "math" "net" "reflect" @@ -13,6 +14,7 @@ import ( "testing" "time" + "github.com/quic-go/webtransport-go" "go.sia.tech/core/consensus" proto4 "go.sia.tech/core/rhp/v4" "go.sia.tech/core/types" @@ -68,6 +70,50 @@ func (fs *fundAndSign) Address() types.Address { return fs.w.Address() } +// webTransportClient wraps a WebTransport session to implement rhp4.TransportClient +type webTransportClient struct { + sess *webtransport.Session + peerKey types.PublicKey +} + +func (c *webTransportClient) Close() error { + return c.sess.CloseWithError(0, "") +} + +func (c *webTransportClient) PeerKey() types.PublicKey { + return c.peerKey +} + +func (c *webTransportClient) FrameSize() int { + return 1440 * 3 +} + +func (c *webTransportClient) DialStream(ctx context.Context) (net.Conn, error) { + stream, err := c.sess.OpenStreamSync(ctx) + if err != nil { + return nil, err + } + return &webTransportStream{ + Stream: stream, + localAddr: c.sess.LocalAddr(), + remoteAddr: c.sess.RemoteAddr(), + }, nil +} + +// webTransportStream wraps a WebTransport stream to implement net.Conn +type webTransportStream struct { + *webtransport.Stream + localAddr, remoteAddr net.Addr +} + +func (s *webTransportStream) LocalAddr() net.Addr { + return s.localAddr +} + +func (s *webTransportStream) RemoteAddr() net.Addr { + return s.remoteAddr +} + func testRenterHostPairSiaMux(tb testing.TB, hostKey types.PrivateKey, cm rhp4.ChainManager, w rhp4.Wallet, c rhp4.Contractor, sr rhp4.Settings, ss rhp4.Sectors, log *zap.Logger) rhp4.TransportClient { rs := rhp4.NewServer(hostKey, cm, c, w, sr, ss, rhp4.WithPriceTableValidity(2*time.Minute)) hostAddr := testutil.ServeSiaMux(tb, rs, log.Named("siamux")) @@ -95,6 +141,27 @@ func testRenterHostPairQUIC(tb testing.TB, hostKey types.PrivateKey, cm rhp4.Cha return transport } +func testRenterHostPairWebTransport(tb testing.TB, hostKey types.PrivateKey, cm rhp4.ChainManager, w rhp4.Wallet, c rhp4.Contractor, sr rhp4.Settings, ss rhp4.Sectors, log *zap.Logger) rhp4.TransportClient { + rs := rhp4.NewServer(hostKey, cm, c, w, sr, ss, rhp4.WithPriceTableValidity(2*time.Minute)) + hostAddr := testutil.ServeQUIC(tb, rs, quic.WithServeLogger(log.Named("webtransport"))) + + dialer := webtransport.Dialer{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + tb.Cleanup(func() { dialer.Close() }) + + url := fmt.Sprintf("https://%s/sia/rhp/v4", hostAddr) + _, sess, err := dialer.Dial(context.Background(), url, nil) + if err != nil { + tb.Fatal(err) + } + tb.Cleanup(func() { sess.CloseWithError(0, "") }) + + return &webTransportClient{sess: sess, peerKey: hostKey.PublicKey()} +} + func startTestNode(tb testing.TB, n *consensus.Network, genesis types.Block) (*chain.Manager, *wallet.SingleAddressWallet) { db, tipstate, err := chain.NewDBStore(chain.NewMemDB(), n, genesis, nil) if err != nil { @@ -268,6 +335,11 @@ func TestFormContract(t *testing.T) { transport := testRenterHostPairQUIC(t, hostKey, cm, w, c, sr, ss, zap.NewNop()) testFormContract(t, transport) }) + + t.Run("webtransport", func(t *testing.T) { + transport := testRenterHostPairWebTransport(t, hostKey, cm, w, c, sr, ss, zap.NewNop()) + testFormContract(t, transport) + }) } func TestFormContractBasis(t *testing.T) {