diff --git a/net/handshake/Makefile b/net/handshake/Makefile new file mode 100644 index 00000000000..4530e364432 --- /dev/null +++ b/net/handshake/Makefile @@ -0,0 +1,8 @@ + +all: semver.pb.go + +semver.pb.go: semver.proto + protoc --gogo_out=. --proto_path=../../../../../:/usr/local/opt/protobuf/include:. $< + +clean: + rm semver.pb.go diff --git a/net/handshake/semver.pb.go b/net/handshake/semver.pb.go new file mode 100644 index 00000000000..e1a5fb5d903 --- /dev/null +++ b/net/handshake/semver.pb.go @@ -0,0 +1,51 @@ +// Code generated by protoc-gen-gogo. +// source: semver.proto +// DO NOT EDIT! + +/* +Package handshake is a generated protocol buffer package. + +It is generated from these files: + semver.proto + +It has these top-level messages: + Handshake1 +*/ +package handshake + +import proto "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = math.Inf + +type Handshake1 struct { + // protocolVersion determines compatibility between peers + ProtocolVersion *string `protobuf:"bytes,1,opt,name=protocolVersion" json:"protocolVersion,omitempty"` + // agentVersion is like a UserAgent string in browsers, or client version in bittorrent + // includes the client name and client. e.g. "go-ipfs/0.1.0" + AgentVersion *string `protobuf:"bytes,2,opt,name=agentVersion" json:"agentVersion,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Handshake1) Reset() { *m = Handshake1{} } +func (m *Handshake1) String() string { return proto.CompactTextString(m) } +func (*Handshake1) ProtoMessage() {} + +func (m *Handshake1) GetProtocolVersion() string { + if m != nil && m.ProtocolVersion != nil { + return *m.ProtocolVersion + } + return "" +} + +func (m *Handshake1) GetAgentVersion() string { + if m != nil && m.AgentVersion != nil { + return *m.AgentVersion + } + return "" +} + +func init() { +} diff --git a/net/handshake/semver.proto b/net/handshake/semver.proto new file mode 100644 index 00000000000..5c6ac2b91b7 --- /dev/null +++ b/net/handshake/semver.proto @@ -0,0 +1,12 @@ +package handshake; + +message Handshake1 { + // protocolVersion determines compatibility between peers + optional string protocolVersion = 1; // semver + + // agentVersion is like a UserAgent string in browsers, or client version in bittorrent + // includes the client name and client. e.g. "go-ipfs/0.1.0" + optional string agentVersion = 2; // semver + + // we'll have more fields here later. +} diff --git a/net/handshake/version.go b/net/handshake/version.go new file mode 100644 index 00000000000..34152206243 --- /dev/null +++ b/net/handshake/version.go @@ -0,0 +1,56 @@ +package handshake + +import ( + "errors" + "fmt" + + updates "github.com/jbenet/go-ipfs/updates" + + semver "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/coreos/go-semver/semver" +) + +// currentVersion holds the current protocol version for a client running this code +var currentVersion *semver.Version + +func init() { + var err error + currentVersion, err = semver.NewVersion("0.0.1") + if err != nil { + panic(fmt.Errorf("invalid protocol version: %v", err)) + } +} + +// CurrentHandshake returns the current protocol version as a protobuf message +func CurrentHandshake() *Handshake1 { + return NewHandshake1(currentVersion.String(), "go-ipfs/"+updates.Version) +} + +// ErrVersionMismatch is returned when two clients don't share a protocol version +var ErrVersionMismatch = errors.New("protocol missmatch") + +// Compatible checks wether two versions are compatible +// returns nil if they are fine +func Compatible(handshakeA, handshakeB *Handshake1) error { + a, err := semver.NewVersion(*handshakeA.ProtocolVersion) + if err != nil { + return err + } + b, err := semver.NewVersion(*handshakeB.ProtocolVersion) + if err != nil { + return err + } + + if a.Major != b.Major { + return ErrVersionMismatch + } + + return nil +} + +// NewHandshake1 creates a new Handshake1 from the two strings +func NewHandshake1(protoVer, agentVer string) *Handshake1 { + return &Handshake1{ + ProtocolVersion: &protoVer, + AgentVersion: &agentVer, + } +} diff --git a/net/handshake/version_test.go b/net/handshake/version_test.go new file mode 100644 index 00000000000..de761a7a56e --- /dev/null +++ b/net/handshake/version_test.go @@ -0,0 +1,23 @@ +package handshake + +import "testing" + +func TestCompatible(t *testing.T) { + tcases := []struct { + a, b string + expected error + }{ + {"0.0.0", "0.0.0", nil}, + {"1.0.0", "1.1.0", nil}, + {"1.0.0", "1.0.1", nil}, + {"0.0.0", "1.0.0", ErrVersionMismatch}, + {"1.0.0", "0.0.0", ErrVersionMismatch}, + } + + for i, tcase := range tcases { + + if Compatible(NewHandshake1(tcase.a, ""), NewHandshake1(tcase.b, "")) != tcase.expected { + t.Fatalf("case[%d] failed", i) + } + } +} diff --git a/net/swarm/conn.go b/net/swarm/conn.go index 7d5c47b5c9f..5aa5a304e2d 100644 --- a/net/swarm/conn.go +++ b/net/swarm/conn.go @@ -6,8 +6,10 @@ import ( spipe "github.com/jbenet/go-ipfs/crypto/spipe" conn "github.com/jbenet/go-ipfs/net/conn" + handshake "github.com/jbenet/go-ipfs/net/handshake" msg "github.com/jbenet/go-ipfs/net/message" + proto "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/goprotobuf/proto" ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr" manet "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr/net" ) @@ -109,6 +111,10 @@ func (s *Swarm) connSetup(c *conn.Conn) error { // add address of connection to Peer. Maybe it should happen in connSecure. c.Peer.AddAddress(c.Addr) + if err := s.connVersionExchange(c); err != nil { + return fmt.Errorf("Conn version exchange error: %v", err) + } + // add to conns s.connsLock.Lock() if _, ok := s.conns[c.Peer.Key()]; ok { @@ -152,6 +158,53 @@ func (s *Swarm) connSecure(c *conn.Conn) error { return nil } +// connVersionExchange exchanges local and remote versions and compares them +// closes remote and returns an error in case of major difference +func (s *Swarm) connVersionExchange(remote *conn.Conn) error { + var remoteHandshake, localHandshake *handshake.Handshake1 + localHandshake = handshake.CurrentHandshake() + + myVerBytes, err := proto.Marshal(localHandshake) + if err != nil { + return err + } + + remote.Secure.Out <- myVerBytes + + log.Debug("Send my version(%s) [to = %s]", localHandshake, remote.Peer) + + select { + case <-s.ctx.Done(): + return s.ctx.Err() + + case <-remote.Closed: + return errors.New("remote closed connection during version exchange") + + case data, ok := <-remote.Secure.In: + if !ok { + return fmt.Errorf("Error retrieving from conn: %v", remote.Peer) + } + + remoteHandshake = new(handshake.Handshake1) + err = proto.Unmarshal(data, remoteHandshake) + if err != nil { + s.Close() + return fmt.Errorf("connSetup: could not decode remote version: %q", err) + } + + log.Debug("Received remote version(%s) [from = %s]", remoteHandshake, remote.Peer) + } + + if err := handshake.Compatible(localHandshake, remoteHandshake); err != nil { + log.Info("%s (%s) incompatible version with %s (%s)", s.local, localHandshake, remote.Peer, remoteHandshake) + remote.Close() + return err + } + + log.Debug("[peer: %s] Version compatible", remote.Peer) + return nil +} + // Handles the unwrapping + sending of messages to the right connection. func (s *Swarm) fanOut() { for {