Skip to content

Commit 0ca2c1a

Browse files
authored
feat: version negotiation (#639)
# Description Adding version negotiation between `zipper` and `source/sfn`, if version negotiation failed, the `source/sfn` cannot connect to `zipper`. The version format must follow the `Major.Minor.Patch` formatting, and the Major, Minor, and Patch components must be numeric. The client‘s MAJOR and MINOR versions should equal to server's, otherwise, the zipper will be considered has break-change, the handshake will fail. ## Impact This is a break-change. It will not be possible to establish a connection with the `zipper` if the `source/sfn` does not send the version field.
1 parent 2402767 commit 0ca2c1a

10 files changed

Lines changed: 233 additions & 29 deletions

File tree

core/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ func (c *Client) connect(ctx context.Context, addr string) (frame.Conn, error) {
157157
ObserveDataTags: c.opts.observeDataTags,
158158
AuthName: c.opts.credential.Name(),
159159
AuthPayload: c.opts.credential.Payload(),
160+
Version: Version,
160161
}
161162

162163
if err := conn.WriteFrame(hf); err != nil {

core/context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func newContext(conn *Connection, route router.Route, df *frame.DataFrame) (c *C
119119
return
120120
}
121121

122-
// CloseWithError close dataStream with an error string.
122+
// CloseWithError close connection with an error string.
123123
func (c *Context) CloseWithError(errString string) {
124124
c.Logger.Debug("connection closed", "err", errString)
125125

core/frame/frame.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,23 @@ func (f *DataFrame) Type() Type { return TypeDataFrame }
4545
// It includes essential details required for the creation of a fresh connection.
4646
// The server then generates the connection utilizing this provided information.
4747
type HandshakeFrame struct {
48-
// Name is the name of the dataStream that will be created.
48+
// Name is the name of the connection that will be created.
4949
Name string
50-
// ID is the ID of the dataStream that will be created.
50+
// ID is the ID of the connection that will be created.
5151
ID string
5252
// ClientType is the type of client.
5353
ClientType byte
54-
// ObserveDataTags is the ObserveDataTags of the dataStream that will be created.
54+
// ObserveDataTags is the ObserveDataTags of the connection that will be created.
5555
ObserveDataTags []Tag
5656
// AuthName is the authentication name.
5757
AuthName string
5858
// AuthPayload is the authentication payload.
5959
AuthPayload string
60+
// Version is used by the source/sfn to communicate their version to the server.
61+
// The Version format must follow the `Major.Minor.Patch`. otherwise, the handshake
62+
// will fail. The client‘s MAJOR and MINOR versions should equal to server's,
63+
// otherwise, the zipper will be considered has break-change, the handshake will fail.
64+
Version string
6065
}
6166

6267
// Type returns the type of HandshakeFrame.

core/server.go

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/yomorun/yomo/pkg/frame-codec/y3codec"
2222
yquic "github.com/yomorun/yomo/pkg/listener/quic"
2323
pkgtls "github.com/yomorun/yomo/pkg/tls"
24+
"github.com/yomorun/yomo/pkg/version"
2425
oteltrace "go.opentelemetry.io/otel/trace"
2526
)
2627

@@ -152,7 +153,22 @@ func (s *Server) Serve(ctx context.Context, conn net.PacketConn) error {
152153
}
153154
}
154155

155-
func (s *Server) handshake(fconn frame.Conn) (bool, router.Route, *Connection) {
156+
func (s *Server) handleFrameConn(fconn frame.Conn, logger *slog.Logger) {
157+
route, conn, err := s.handshake(fconn)
158+
if err != nil {
159+
logger.Error("handshake failed", "err", err)
160+
return
161+
}
162+
163+
s.connHandler(conn, route) // s.handleConnRoute(conn, route) with middlewares
164+
165+
if conn.ClientType() == ClientTypeStreamFunction {
166+
_ = route.Remove(conn.ID())
167+
}
168+
_ = s.connector.Remove(conn.ID())
169+
}
170+
171+
func (s *Server) handshake(fconn frame.Conn) (router.Route, *Connection, error) {
156172
var gerr error
157173

158174
defer func() {
@@ -166,7 +182,7 @@ func (s *Server) handshake(fconn frame.Conn) (bool, router.Route, *Connection) {
166182
first, err := fconn.ReadFrame()
167183
if err != nil {
168184
gerr = err
169-
return false, nil, nil
185+
return nil, nil, gerr
170186
}
171187
switch first.Type() {
172188
case frame.TypeHandshakeFrame:
@@ -175,17 +191,17 @@ func (s *Server) handshake(fconn frame.Conn) (bool, router.Route, *Connection) {
175191
conn, err := s.handleHandshakeFrame(fconn, hf)
176192
if err != nil {
177193
gerr = err
178-
return false, nil, conn
194+
return nil, conn, gerr
179195
}
180196

181197
route, err := s.addSfnToRoute(hf, conn.Metadata())
182198
if err != nil {
183199
gerr = err
184200
}
185-
return true, route, conn
201+
return route, conn, gerr
186202
default:
187203
gerr = fmt.Errorf("yomo: handshake read unexpected frame, read: %s", first.Type().String())
188-
return false, nil, nil
204+
return nil, nil, gerr
189205
}
190206
}
191207

@@ -216,22 +232,8 @@ func (s *Server) handleConnRoute(conn *Connection, route router.Route) {
216232
}
217233
}
218234

219-
func (s *Server) handleFrameConn(fconn frame.Conn, logger *slog.Logger) {
220-
ok, route, conn := s.handshake(fconn)
221-
if !ok {
222-
logger.Error("handshake failed")
223-
return
224-
}
225-
226-
s.connHandler(conn, route) // s.handleConnRoute(conn, route) with middlewares
227-
228-
if conn.ClientType() == ClientTypeStreamFunction {
229-
_ = route.Remove(conn.ID())
230-
}
231-
_ = s.connector.Remove(conn.ID())
232-
}
233-
234235
func (s *Server) handleHandshakeFrame(fconn frame.Conn, hf *frame.HandshakeFrame) (*Connection, error) {
236+
// 1. authentication
235237
md, ok := auth.Authenticate(s.opts.auths, hf)
236238

237239
if !ok {
@@ -243,6 +245,11 @@ func (s *Server) handleHandshakeFrame(fconn frame.Conn, hf *frame.HandshakeFrame
243245
return nil, fmt.Errorf("authentication failed: client credential type is %s", hf.AuthName)
244246
}
245247

248+
// 2. version negotiation
249+
if err := negotiateVersion(hf.Version, Version); err != nil {
250+
return nil, err
251+
}
252+
246253
conn := newConnection(hf.Name, hf.ID, ClientType(hf.ClientType), md, hf.ObserveDataTags, fconn, s.logger)
247254

248255
return conn, s.connector.Store(hf.ID, conn)
@@ -263,6 +270,25 @@ func (s *Server) addSfnToRoute(hf *frame.HandshakeFrame, md metadata.M) (router.
263270
return route, nil
264271
}
265272

273+
func negotiateVersion(cVersion, sVersion string) error {
274+
cv, err := version.Parse(cVersion)
275+
if err != nil {
276+
return err
277+
}
278+
279+
sv, err := version.Parse(sVersion)
280+
if err != nil {
281+
return err
282+
}
283+
284+
// If the Major and Minor versions are equal, the server can serve the client.
285+
if cv.Major == sv.Major && cv.Minor == sv.Minor {
286+
return nil
287+
}
288+
289+
return fmt.Errorf("yomo: version negotiation failed, client=%s, server=%s", cVersion, sVersion)
290+
}
291+
266292
func (s *Server) handleFrame(c *Context) {
267293
// routing data frame.
268294
if err := s.routingDataFrame(c); err != nil {

core/server_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package core
22

33
import (
4+
"errors"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
@@ -44,3 +45,46 @@ func (s *mockConnectionInfo) Name() string { return s.name }
4445
func (s *mockConnectionInfo) Metadata() metadata.M { return s.metadata }
4546
func (s *mockConnectionInfo) ClientType() ClientType { return s.clientType }
4647
func (s *mockConnectionInfo) ObserveDataTags() []frame.Tag { return s.observed }
48+
49+
func Test_negotiateVersion(t *testing.T) {
50+
type args struct {
51+
cVersion string
52+
sVersion string
53+
}
54+
tests := []struct {
55+
name string
56+
args args
57+
wantErr error
58+
}{
59+
{
60+
name: "ok",
61+
args: args{
62+
cVersion: "1.16.3",
63+
sVersion: "1.16.3",
64+
},
65+
wantErr: nil,
66+
},
67+
{
68+
name: "client empty version",
69+
args: args{
70+
cVersion: "",
71+
sVersion: "1.16.3",
72+
},
73+
wantErr: errors.New("invalid semantic version, params="),
74+
},
75+
{
76+
name: "not ok",
77+
args: args{
78+
cVersion: "1.15.0",
79+
sVersion: "1.16.3",
80+
},
81+
wantErr: errors.New("yomo: version negotiation failed, client=1.15.0, server=1.16.3"),
82+
},
83+
}
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
err := negotiateVersion(tt.args.cVersion, tt.args.sVersion)
87+
assert.Equal(t, tt.wantErr, err)
88+
})
89+
}
90+
}

core/version.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package core
2+
3+
// Version is the current yomo version.
4+
const Version = "1.17.0"

pkg/frame-codec/y3codec/codec_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ func TestCodec(t *testing.T) {
7676
ObserveDataTags: []uint32{'a', 'b', 'c'},
7777
AuthName: "ddddd",
7878
AuthPayload: "eeeee",
79+
Version: "1.16.3",
7980
},
80-
data: []byte{0xb1, 0x31, 0x1, 0x8, 0x74, 0x68, 0x65, 0x2d, 0x6e, 0x61,
81+
data: []byte{0xb1, 0x39, 0x1, 0x8, 0x74, 0x68, 0x65, 0x2d, 0x6e, 0x61,
8182
0x6d, 0x65, 0x3, 0x6, 0x74, 0x68, 0x65, 0x2d, 0x69, 0x64, 0x2, 0x1,
8283
0x68, 0x6, 0xc, 0x61, 0x0, 0x0, 0x0, 0x62, 0x0, 0x0, 0x0, 0x63, 0x0,
83-
0x0, 0x0, 0x4, 0x5, 0x64, 0x64, 0x64, 0x64, 0x64, 0x5, 0x5, 0x65,
84-
0x65, 0x65, 0x65, 0x65,
85-
},
84+
0x0, 0x0, 0x4, 0x5, 0x64, 0x64, 0x64, 0x64, 0x64, 0x5, 0x5, 0x65, 0x65,
85+
0x65, 0x65, 0x65, 0x7, 0x6, 0x31, 0x2e, 0x31, 0x36, 0x2e, 0x33},
8686
},
8787
},
8888
{

pkg/frame-codec/y3codec/handshake_frame.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ func encodeHandshakeFrame(f *frame.HandshakeFrame) ([]byte, error) {
3131
// auth payload
3232
authPayloadBlock := y3.NewPrimitivePacketEncoder(tagAuthenticationPayload)
3333
authPayloadBlock.SetStringValue(f.AuthPayload)
34+
// version
35+
versionBlock := y3.NewPrimitivePacketEncoder(tagHandshakeVersion)
36+
versionBlock.SetStringValue(f.Version)
3437

3538
// handshake frame
3639
handshake := y3.NewNodePacketEncoder(byte(f.Type()))
@@ -40,6 +43,7 @@ func encodeHandshakeFrame(f *frame.HandshakeFrame) ([]byte, error) {
4043
handshake.AddPrimitivePacket(observeDataTagsBlock)
4144
handshake.AddPrimitivePacket(authNameBlock)
4245
handshake.AddPrimitivePacket(authPayloadBlock)
46+
handshake.AddPrimitivePacket(versionBlock)
4347

4448
return handshake.Encode(), nil
4549
}
@@ -98,15 +102,24 @@ func decodeHandshakeFrame(data []byte, f *frame.HandshakeFrame) error {
98102
}
99103
f.AuthPayload = authPayload
100104
}
105+
// version
106+
if versionBlock, ok := node.PrimitivePackets[tagHandshakeVersion]; ok {
107+
version, err := versionBlock.ToUTF8String()
108+
if err != nil {
109+
return err
110+
}
111+
f.Version = version
112+
}
101113

102114
return nil
103115
}
104116

105-
var (
117+
const (
106118
tagHandshakeName byte = 0x01
107119
tagHandshakeClientType byte = 0x02
108120
tagHandshakeID byte = 0x03
109121
tagAuthenticationName byte = 0x04
110122
tagAuthenticationPayload byte = 0x05
111123
tagHandshakeObserveDataTags byte = 0x06
124+
tagHandshakeVersion byte = 0x07
112125
)

pkg/version/version.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Package version provides functionality for parsing versions..
2+
package version
3+
4+
import (
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
// Version is used by the source/sfn to communicate their version to the server.
11+
type Version struct {
12+
Major int
13+
Minor int
14+
Patch int
15+
}
16+
17+
// Parse parses a string into a Version. The string format must follow the `Major.Minor.Patch`
18+
// formatting, and the Major, Minor, and Patch components must be numeric. If they are not,
19+
// a parse error will be returned.
20+
func Parse(str string) (*Version, error) {
21+
vs := strings.Split(str, ".")
22+
if len(vs) != 3 {
23+
return nil, fmt.Errorf("invalid semantic version, params=%s", str)
24+
}
25+
26+
major, err := strconv.Atoi(vs[0])
27+
if err != nil {
28+
return nil, fmt.Errorf("invalid version major, params=%s", str)
29+
}
30+
31+
minor, err := strconv.Atoi(vs[1])
32+
if err != nil {
33+
return nil, fmt.Errorf("invalid version minor, params=%s", str)
34+
}
35+
36+
patch, err := strconv.Atoi(vs[2])
37+
if err != nil {
38+
return nil, fmt.Errorf("invalid version patch, params=%s", str)
39+
}
40+
41+
ver := &Version{Major: major, Minor: minor, Patch: patch}
42+
43+
return ver, nil
44+
}

0 commit comments

Comments
 (0)