Skip to content

Commit cf1b795

Browse files
committed
2026-05-28 13:25:10
1 parent d87ca3f commit cf1b795

5 files changed

Lines changed: 188 additions & 1192 deletions

File tree

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
module github.com/libraries/daze
22

33
go 1.26.0
4+
5+
require golang.org/x/net v0.55.0
6+
7+
require (
8+
golang.org/x/crypto v0.51.0 // indirect
9+
golang.org/x/sys v0.45.0 // indirect
10+
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
2+
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
3+
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
4+
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
5+
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
6+
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

protocol/etch/engine.go

Lines changed: 175 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,113 @@
11
package etch
22

33
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/rand"
8+
"crypto/tls"
9+
"crypto/x509"
10+
"errors"
411
"io"
512
"log"
613
"math"
14+
"math/big"
15+
"net"
16+
"sync"
717
"time"
818

919
"github.com/libraries/daze"
20+
"github.com/libraries/daze/lib/doa"
21+
"github.com/libraries/daze/lib/once"
1022
"github.com/libraries/daze/lib/rate"
1123
"github.com/libraries/daze/protocol/ashe"
24+
"golang.org/x/net/quic"
1225
)
1326

14-
// The etch engine is the ashe protocol carried over the etch rudp transport. It exists for the same reason as the
15-
// czar engine: to let the ashe protocol travel over a transport that is not vanilla tcp. The benefit of moving ashe
16-
// onto rudp is that a single udp flow is much harder for middleboxes to throttle than a long-lived tcp connection, and
17-
// the connection survives transient packet loss without giving up.
27+
var (
28+
ServerConfig = once.NewOnceNew(func() *quic.Config {
29+
key := doa.Try(ecdsa.GenerateKey(elliptic.P256(), rand.Reader))
30+
tpl := &x509.Certificate{
31+
SerialNumber: big.NewInt(1),
32+
NotBefore: time.Now().Add(-time.Hour),
33+
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 10),
34+
}
35+
der := doa.Try(x509.CreateCertificate(rand.Reader, tpl, tpl, &key.PublicKey, key))
36+
return &quic.Config{
37+
TLSConfig: &tls.Config{
38+
Certificates: []tls.Certificate{{Certificate: [][]byte{der}, PrivateKey: key}},
39+
MinVersion: tls.VersionTLS13,
40+
NextProtos: []string{"ashe"},
41+
},
42+
MaxBidiRemoteStreams: math.MaxInt32,
43+
MaxStreamReadBufferSize: 4 * 1024 * 1024,
44+
MaxStreamWriteBufferSize: 4 * 1024 * 1024,
45+
MaxConnReadBufferSize: 4 * 1024 * 1024,
46+
MaxIdleTimeout: -1,
47+
KeepAlivePeriod: time.Second * 30,
48+
}
49+
})
50+
ClientConfig = once.NewOnceNew(func() *quic.Config {
51+
return &quic.Config{
52+
TLSConfig: &tls.Config{
53+
InsecureSkipVerify: true,
54+
MinVersion: tls.VersionTLS13,
55+
NextProtos: []string{"ashe"},
56+
},
57+
MaxBidiRemoteStreams: math.MaxInt32,
58+
MaxStreamReadBufferSize: 4 * 1024 * 1024,
59+
MaxStreamWriteBufferSize: 4 * 1024 * 1024,
60+
MaxConnReadBufferSize: 4 * 1024 * 1024,
61+
MaxIdleTimeout: -1,
62+
KeepAlivePeriod: time.Second * 30,
63+
}
64+
})
65+
)
66+
67+
// Stream wraps a quic stream as an io.ReadWriteCloser. Writes are flushed immediately so that the ashe handshake,
68+
// which exchanges very short messages, progresses without waiting for the quic stream buffer to fill.
69+
type Stream struct {
70+
con *quic.Conn
71+
rem net.Addr
72+
stm *quic.Stream
73+
}
74+
75+
// Close closes the stream. If the stream owns its connection, the connection is closed as well.
76+
func (s *Stream) Close() error {
77+
s.stm.CloseRead()
78+
s.stm.CloseWrite()
79+
if s.con != nil {
80+
return s.con.Close()
81+
}
82+
return nil
83+
}
84+
85+
// Read reads up to len(p) bytes.
86+
func (s *Stream) Read(p []byte) (int, error) {
87+
return s.stm.Read(p)
88+
}
89+
90+
// RemoteAddr returns the remote network address.
91+
func (s *Stream) RemoteAddr() net.Addr {
92+
return s.rem
93+
}
94+
95+
// Write writes len(p) bytes and flushes the stream so the data reaches the wire immediately.
96+
func (s *Stream) Write(p []byte) (int, error) {
97+
n, err := s.stm.Write(p)
98+
if err != nil {
99+
return n, err
100+
}
101+
if err := s.stm.Flush(); err != nil {
102+
return n, err
103+
}
104+
return n, nil
105+
}
18106

19-
// Server implemented the ashe-over-etch protocol.
107+
// Server implemented the ashe-over-quic protocol.
20108
type Server struct {
21109
Cipher []byte
22-
Closer io.Closer
110+
Ep *quic.Endpoint
23111
Limits *rate.Limits
24112
Listen string
25113
}
@@ -32,39 +120,57 @@ func (s *Server) Serve(ctx *daze.Context, cli io.ReadWriteCloser) error {
32120

33121
// Close listener. Established connections will not be closed.
34122
func (s *Server) Close() error {
35-
if s.Closer != nil {
36-
return s.Closer.Close()
123+
if s.Ep != nil {
124+
return s.Ep.Close(context.Background())
37125
}
38126
return nil
39127
}
40128

41129
// Run it.
42130
func (s *Server) Run() error {
43-
l, err := Listen(s.Listen)
131+
ep, err := quic.Listen("udp", s.Listen, ServerConfig.Do())
44132
if err != nil {
45133
return err
46134
}
47-
s.Closer = l
135+
s.Ep = ep
48136
log.Println("main: listen and serve on", s.Listen)
49137

50138
go func() {
51139
idx := uint32(math.MaxUint32)
52-
for cli := range l.Accept() {
53-
idx++
54-
ctx := &daze.Context{Cid: idx}
55-
log.Printf("conn: %08x accept remote=%s", ctx.Cid, cli.RemoteAddr())
56-
rtc := &daze.ReadWriteCloser{
57-
Reader: io.TeeReader(cli, rate.NewLimitsWriter(s.Limits)),
58-
Writer: io.MultiWriter(cli, rate.NewLimitsWriter(s.Limits)),
59-
Closer: cli,
140+
for {
141+
con, err := ep.Accept(context.Background())
142+
if err != nil {
143+
if !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) {
144+
log.Println("main:", err)
145+
}
146+
break
60147
}
61-
go func() {
62-
defer rtc.Close()
63-
if err := s.Serve(ctx, rtc); err != nil {
64-
log.Printf("conn: %08x error %s", ctx.Cid, err)
148+
go func(con *quic.Conn) {
149+
defer con.Close()
150+
peer := net.UDPAddrFromAddrPort(con.RemoteAddr())
151+
for {
152+
stm, err := con.AcceptStream(context.Background())
153+
if err != nil {
154+
return
155+
}
156+
idx++
157+
ctx := &daze.Context{Cid: idx}
158+
cli := &Stream{rem: peer, stm: stm}
159+
log.Printf("conn: %08x accept remote=%s", ctx.Cid, peer)
160+
rtc := &daze.ReadWriteCloser{
161+
Reader: io.TeeReader(cli, rate.NewLimitsWriter(s.Limits)),
162+
Writer: io.MultiWriter(cli, rate.NewLimitsWriter(s.Limits)),
163+
Closer: cli,
164+
}
165+
go func() {
166+
defer rtc.Close()
167+
if err := s.Serve(ctx, rtc); err != nil {
168+
log.Printf("conn: %08x error %s", ctx.Cid, err)
169+
}
170+
log.Printf("conn: %08x closed", ctx.Cid)
171+
}()
65172
}
66-
log.Printf("conn: %08x closed", ctx.Cid)
67-
}()
173+
}(con)
68174
}
69175
}()
70176

@@ -80,35 +186,71 @@ func NewServer(listen string, cipher string) *Server {
80186
}
81187
}
82188

83-
// Client implemented the ashe-over-etch protocol.
189+
// Client implemented the ashe-over-quic protocol.
84190
type Client struct {
85191
Cipher []byte
86192
Server string
193+
194+
mu sync.Mutex
195+
ep *quic.Endpoint
196+
}
197+
198+
// endpoint lazily creates the shared quic endpoint that originates outbound connections. A single endpoint can
199+
// originate many connections, so reusing it avoids burning a new udp socket for every Dial.
200+
func (c *Client) endpoint() (*quic.Endpoint, error) {
201+
c.mu.Lock()
202+
defer c.mu.Unlock()
203+
if c.ep != nil {
204+
return c.ep, nil
205+
}
206+
// The client endpoint also needs a tls config that satisfies the "certificate or GetCertificate" requirement of
207+
// quic.Listen even though the endpoint never accepts inbound connections.
208+
ep, err := quic.Listen("udp", ":0", ClientConfig.Do())
209+
if err != nil {
210+
return nil, err
211+
}
212+
c.ep = ep
213+
return ep, nil
87214
}
88215

89216
// Estab dials the etch server and establishes an ashe channel over it. It is the caller's responsibility to close the
90217
// returned conn.
91218
func (c *Client) Estab(ctx *daze.Context, network string, address string) (io.ReadWriteCloser, error) {
92-
srv, err := Dial(c.Server)
219+
ep, err := c.endpoint()
93220
if err != nil {
94221
return nil, err
95222
}
223+
dialCtx, cancel := context.WithTimeout(context.Background(), daze.Conf.DialerTimeout)
224+
defer cancel()
225+
con, err := ep.Dial(dialCtx, "udp", c.Server, ClientConfig.Do())
226+
if err != nil {
227+
return nil, err
228+
}
229+
stm, err := con.NewStream(dialCtx)
230+
if err != nil {
231+
con.Close()
232+
return nil, err
233+
}
234+
// NewStream is lazy: the peer does not see the stream until the first byte is written. Force a flush so the
235+
// server's AcceptStream returns promptly even if the ashe client decides to read before writing.
236+
if err := stm.Flush(); err != nil {
237+
con.Close()
238+
return nil, err
239+
}
240+
addr := net.UDPAddrFromAddrPort(con.RemoteAddr())
241+
srv := &Stream{con: con, rem: addr, stm: stm}
96242
spy := &ashe.Client{Cipher: c.Cipher}
97-
con, err := spy.Estab(ctx, srv, network, address)
243+
out, err := spy.Estab(ctx, srv, network, address)
98244
if err != nil {
99245
srv.Close()
100246
return nil, err
101247
}
102-
return con, nil
248+
return out, nil
103249
}
104250

105251
// Dial connects to the address on the named network.
106252
func (c *Client) Dial(ctx *daze.Context, network string, address string) (io.ReadWriteCloser, error) {
107-
con, err := c.Estab(ctx, network, address)
108-
if err != nil {
109-
return nil, err
110-
}
111-
return con, nil
253+
return c.Estab(ctx, network, address)
112254
}
113255

114256
// NewClient returns a new Client. Cipher is a password in string form, with no length limit.

0 commit comments

Comments
 (0)