diff --git a/router/connection.go b/router/connection.go index 5bc3a08a..65c18935 100644 --- a/router/connection.go +++ b/router/connection.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "fmt" "net" + "strconv" "sync" "time" @@ -117,6 +118,7 @@ func StartLocalConnection(connRemote *RemoteConnection, tcpConn *net.TCPConn, ud TCPConn: tcpConn, remoteUDPAddr: udpAddr, effectivePMTU: DefaultPMTU, + uid: randUint64(), actionChan: actionChan, finished: finished} go conn.run(actionChan, finished, acceptNewPeer) @@ -282,8 +284,21 @@ func (conn *LocalConnection) run(actionChan <-chan ConnectionAction, finished ch defer close(finished) conn.TCPConn.SetLinger(0) + intro, err := ProtocolIntroParams{ + Features: conn.makeFeatures(), + Conn: conn.TCPConn, + Password: conn.Router.Password, + Outbound: conn.outbound, + }.DoIntro() + if err != nil { + return + } - tcpReceiver, remote, err := conn.handshake() + conn.SessionKey = intro.SessionKey + conn.tcpSender = intro.Sender + conn.version = intro.Version + + remote, err := conn.parseFeatures(intro.Features) if err != nil { return } @@ -292,7 +307,13 @@ func (conn *LocalConnection) run(actionChan <-chan ConnectionAction, finished ch return } - conn.Log("completed handshake; using protocol version", conn.version) + if conn.SessionKey == nil { + conn.Decryptor = NewNonDecryptor() + } else { + conn.Decryptor = NewNaClDecryptor(conn.SessionKey, conn.outbound) + } + + conn.Log("connection ready; using protocol version", conn.version) // The ordering of the following is very important. [1] @@ -307,7 +328,8 @@ func (conn *LocalConnection) run(actionChan <-chan ConnectionAction, finished ch if err = conn.initHeartbeats(); err != nil { return } - go conn.receiveTCP(tcpReceiver) + + go conn.receiveTCP(intro.Receiver) err = conn.actorLoop(actionChan) } @@ -356,6 +378,76 @@ func (conn *LocalConnection) run(actionChan <-chan ConnectionAction, finished ch // prevent that completely, since, for example, forwarder can only be // created when we know the remote UDP address, but it helps to try. +func (conn *LocalConnection) makeFeatures() map[string]string { + return map[string]string{ + "PeerNameFlavour": PeerNameFlavour, + "Name": conn.local.Name.String(), + "NickName": conn.local.NickName, + "UID": fmt.Sprint(conn.local.UID), + "ConnID": fmt.Sprint(conn.uid), + } +} + +type features map[string]string + +func (features features) Get(key string) (string, error) { + val, ok := features[key] + if !ok { + return "", fmt.Errorf("Field %s is missing", key) + } + + return val, nil +} + +func (conn *LocalConnection) parseFeatures(features features) (*Peer, error) { + str, err := features.Get("PeerNameFlavour") + if err != nil { + return nil, err + } + + if str != PeerNameFlavour { + return nil, fmt.Errorf("Peer name flavour mismatch (ours: \"%s\", theirs: \"%s\")", PeerNameFlavour, str) + } + + str, err = features.Get("Name") + if err != nil { + return nil, err + } + + name, err := PeerNameFromString(str) + if err != nil { + return nil, err + } + + nickName, err := features.Get("NickName") + if err != nil { + return nil, err + } + + str, err = features.Get("UID") + if err != nil { + return nil, err + } + + uid, err := ParsePeerUID(str) + if err != nil { + return nil, err + } + + str, err = features.Get("ConnID") + if err != nil { + return nil, err + } + + remoteConnID, err := strconv.ParseUint(str, 10, 64) + if err != nil { + return nil, err + } + + conn.uid ^= remoteConnID + return NewPeer(name, nickName, uid, 0), nil +} + func (conn *LocalConnection) registerRemote(remote *Peer, acceptNewPeer bool) error { if acceptNewPeer { conn.remote = conn.Router.Peers.FetchWithDefault(remote) diff --git a/router/consts.go b/router/consts.go index 1909498c..1acfc18b 100644 --- a/router/consts.go +++ b/router/consts.go @@ -24,7 +24,6 @@ const ( PMTUVerifyTimeout = 10 * time.Millisecond // gets doubled with every attempt MaxDuration = time.Duration(math.MaxInt64) MaxMissedHeartbeats = 6 - HeaderTimeout = 10 * time.Second HeartbeatTimeout = MaxMissedHeartbeats * SlowHeartbeat ) diff --git a/router/handshake.go b/router/handshake.go deleted file mode 100644 index 0e39585f..00000000 --- a/router/handshake.go +++ /dev/null @@ -1,148 +0,0 @@ -package router - -import ( - "encoding/gob" - "encoding/hex" - "fmt" - "strconv" - "time" -) - -type FieldValidator struct { - fields map[string]string - err error -} - -func NewFieldValidator(fields map[string]string) *FieldValidator { - return &FieldValidator{fields, nil} -} - -func (fv *FieldValidator) Value(fieldName string) (string, error) { - if fv.err != nil { - return "", fv.err - } - val, found := fv.fields[fieldName] - if !found { - fv.err = fmt.Errorf("Field %s is missing", fieldName) - return "", fv.err - } - return val, nil -} - -func (fv *FieldValidator) CheckEqual(fieldName, expectedValue string) error { - val, err := fv.Value(fieldName) - if err != nil { - return err - } - if val != expectedValue { - fv.err = fmt.Errorf("Field %s has wrong value; expected '%s', received '%s'", fieldName, expectedValue, val) - return fv.err - } - return nil -} - -func (fv *FieldValidator) Err() error { - return fv.err -} - -func (conn *LocalConnection) handshake() (TCPReceiver, *Peer, error) { - // We do not need to worry about locking in here as at this point - // the connection is not reachable by any go-routine other than - // ourself. Only when we add this connection to the conn.local - // peer will it be visible from multiple go-routines. - - conn.TCPConn.SetDeadline(time.Now().Add(HeaderTimeout)) - version, err := exchangeProtocolHeader(conn.TCPConn, conn.TCPConn) - if err != nil { - return nil, nil, err - } - conn.version = version - conn.TCPConn.SetDeadline(time.Time{}) - conn.extendReadDeadline() - - localConnID := randUint64() - usingPassword := conn.Router.UsingPassword() - enc := gob.NewEncoder(conn.TCPConn) - dec := gob.NewDecoder(conn.TCPConn) - fv, private, err := conn.exchangeHandshake(localConnID, usingPassword, enc, dec) - if err != nil { - return nil, nil, err - } - - nameStr, _ := fv.Value("Name") - nickNameStr, _ := fv.Value("NickName") - uidStr, _ := fv.Value("UID") - remoteConnIDStr, _ := fv.Value("ConnID") - if err := fv.Err(); err != nil { - return nil, nil, err - } - - name, err := PeerNameFromString(nameStr) - if err != nil { - return nil, nil, err - } - uid, err := ParsePeerUID(uidStr) - if err != nil { - return nil, nil, err - } - remoteConnID, err := strconv.ParseUint(remoteConnIDStr, 10, 64) - if err != nil { - return nil, nil, err - } - conn.uid = localConnID ^ remoteConnID - - var tcpReceiver TCPReceiver - remotePublicStr, rpErr := fv.Value("PublicKey") - if usingPassword { - if rpErr != nil { - return nil, nil, rpErr - } - remotePublicSlice, rpErr := hex.DecodeString(remotePublicStr) - if rpErr != nil { - return nil, nil, rpErr - } - remotePublic := [32]byte{} - for idx, elem := range remotePublicSlice { - remotePublic[idx] = elem - } - conn.SessionKey = FormSessionKey(&remotePublic, private, conn.Router.Password) - conn.tcpSender = NewEncryptedTCPSender(enc, conn.SessionKey, conn.outbound) - tcpReceiver = NewEncryptedTCPReceiver(dec, conn.SessionKey, conn.outbound) - conn.Decryptor = NewNaClDecryptor(conn.SessionKey, conn.outbound) - } else { - if rpErr == nil { - return nil, nil, fmt.Errorf("Remote network is encrypted. Password required.") - } - conn.tcpSender = NewSimpleTCPSender(enc) - tcpReceiver = NewSimpleTCPReceiver(dec) - conn.Decryptor = NewNonDecryptor() - } - - return tcpReceiver, NewPeer(name, nickNameStr, uid, 0), nil -} - -func (conn *LocalConnection) exchangeHandshake(localConnID uint64, usingPassword bool, enc *gob.Encoder, dec *gob.Decoder) (*FieldValidator, *[32]byte, error) { - handshakeSend := map[string]string{ - "PeerNameFlavour": PeerNameFlavour, - "Name": conn.local.Name.String(), - "NickName": conn.local.NickName, - "UID": fmt.Sprint(conn.local.UID), - "ConnID": fmt.Sprint(localConnID)} - handshakeRecv := map[string]string{} - var public, private *[32]byte - var err error - if usingPassword { - public, private, err = GenerateKeyPair() - if err != nil { - return nil, nil, err - } - handshakeSend["PublicKey"] = hex.EncodeToString(public[:]) - } - enc.Encode(handshakeSend) - if err = dec.Decode(&handshakeRecv); err != nil { - return nil, nil, err - } - fv := NewFieldValidator(handshakeRecv) - fv.CheckEqual("PeerNameFlavour", PeerNameFlavour) - return fv, private, nil -} diff --git a/router/handshake_test.go b/router/handshake_test.go deleted file mode 100644 index 2139e080..00000000 --- a/router/handshake_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package router - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestFieldValidator(t *testing.T) { - testMap := map[string]string{"a": "a"} - - fv := NewFieldValidator(testMap) - val, err := fv.Value("a") - require.NoError(t, err) - require.NoError(t, fv.Err()) - require.Equal(t, "a", val, "") - _, err = fv.Value("x") - require.False(t, err == nil || fv.Err() == nil, "Expected error") - _, err = fv.Value("a") - require.False(t, err == nil || fv.Err() == nil, "Previous error should be retained") - - fv = NewFieldValidator(testMap) - err = fv.CheckEqual("a", "a") - require.NoError(t, err) - require.NoError(t, fv.Err()) - err = fv.CheckEqual("a", "b") - require.False(t, err == nil || fv.Err() == nil, "Expected error") - err = fv.CheckEqual("a", "a") - require.False(t, err == nil || fv.Err() == nil, "Previous error should be retained") -} diff --git a/router/protocol.go b/router/protocol.go index bb2ebfa0..58af0da4 100644 --- a/router/protocol.go +++ b/router/protocol.go @@ -2,56 +2,335 @@ package router import ( "bytes" + "encoding/gob" + "encoding/hex" "fmt" "io" + "time" ) const ( Protocol = "weave" - ProtocolVersion byte = 1 - ProtocolMinVersion byte = ProtocolVersion + ProtocolMinVersion byte = 1 + ProtocolVersion byte = 2 ) var ( - ProtocolBytes = []byte(Protocol) - ProtocolLen = len(ProtocolBytes) - ProtocolHeader = append(ProtocolBytes, ProtocolMinVersion, ProtocolVersion) - ProtocolHeaderLen = len(ProtocolHeader) + ProtocolBytes = []byte(Protocol) + + HeaderTimeout = 10 * time.Second + + ProtocolV1Features = []string{ + "ConnID", + "Name", + "NickName", + "PeerNameFlavour", + "UID", + } + + ErrExpectedCrypto = fmt.Errorf("Password specified, but peer requested an unencrypted connection") + ErrExpectedNoCrypto = fmt.Errorf("No password specificed, but peer requested an encrypted connection") ) -func exchangeProtocolHeader(w io.Writer, r io.Reader) (byte, error) { - if n, err := w.Write(ProtocolHeader); err != nil { - return 0, err - } else if n != ProtocolHeaderLen { - return 0, fmt.Errorf("failed to send complete protocol header") +// We don't need the full net.TCPConn to do the protocol intro. This +// interface contains just the parts we do need, to support testing +type ProtocolIntroConn interface { + // io.Reader + Read(b []byte) (n int, err error) + + // io.Writer + Write(b []byte) (n int, err error) + + // net.Conn's deadline methods + SetDeadline(t time.Time) error + SetReadDeadline(t time.Time) error + SetWriteDeadline(t time.Time) error +} + +type ProtocolIntroParams struct { + Features map[string]string + Conn ProtocolIntroConn + Password []byte + Outbound bool +} + +type ProtocolIntroResults struct { + Features map[string]string + Receiver TCPReceiver + Sender TCPSender + SessionKey *[32]byte + Version byte +} + +func (params ProtocolIntroParams) DoIntro() (res ProtocolIntroResults, err error) { + return params.doIntro(ProtocolVersion) +} + +func (params ProtocolIntroParams) doIntro(maxVersion byte) (res ProtocolIntroResults, err error) { + if err = params.Conn.SetDeadline(time.Now().Add(HeaderTimeout)); err != nil { + return } - header := make([]byte, ProtocolHeaderLen) - if n, err := io.ReadFull(r, header); err != nil && n == 0 { + + if res.Version, err = params.exchangeProtocolHeader(maxVersion); err != nil { + return + } + + var pubKey, privKey *[32]byte + if params.Password != nil { + if pubKey, privKey, err = GenerateKeyPair(); err != nil { + return + } + } + + if err = params.Conn.SetWriteDeadline(time.Time{}); err != nil { + return + } + if err = params.Conn.SetReadDeadline(time.Now().Add(TCPHeartbeat * 2)); err != nil { + return + } + + switch res.Version { + case 1: + err = res.doIntroV1(params, pubKey, privKey) + case 2: + err = res.doIntroV2(params, pubKey, privKey) + default: + panic("unhandled protocol version") + } + + return +} + +func (params ProtocolIntroParams) exchangeProtocolHeader(maxVersion byte) (byte, error) { + // Write in a separate goroutine to avoid the possibility of + // deadlock. The result channel is of size 1 so that the + // goroutine does not linger even if we encounter an error on + // the read side. + sendHeader := append(ProtocolBytes, ProtocolMinVersion, maxVersion) + writeDone := make(chan error, 1) + go func() { + _, err := params.Conn.Write(sendHeader) + writeDone <- err + }() + + header := make([]byte, len(ProtocolBytes)+2) + if n, err := io.ReadFull(params.Conn, header); err != nil && n == 0 { return 0, fmt.Errorf("failed to receive remote protocol header: %s", err) } else if err != nil { return 0, fmt.Errorf("received incomplete remote protocol header (%d octets instead of %d): %v; error: %s", - n, ProtocolHeaderLen, header[:n], err) + n, len(header), header[:n], err) } - if !bytes.Equal(ProtocolBytes, header[:ProtocolLen]) { - return 0, fmt.Errorf("remote protocol header not recognised: %v", header[:ProtocolHeaderLen]) + + if !bytes.Equal(ProtocolBytes, header[:len(ProtocolBytes)]) { + return 0, fmt.Errorf("remote protocol header not recognised: %v", header[:len(ProtocolBytes)]) } - var ( - remoteMinVersion = header[ProtocolLen] - remoteVersion = header[ProtocolLen+1] - ) - minVersion := ProtocolMinVersion - if remoteMinVersion > minVersion { - minVersion = remoteMinVersion + + theirMinVersion := header[len(ProtocolBytes)] + minVersion := theirMinVersion + if ProtocolMinVersion > minVersion { + minVersion = ProtocolMinVersion } - maxVersion := ProtocolVersion - if remoteVersion < maxVersion { - maxVersion = remoteVersion + + theirVersion := header[len(ProtocolBytes)+1] + version := theirVersion + if version > maxVersion { + version = maxVersion } - if minVersion > maxVersion { - return 0, fmt.Errorf("remote version [%d,%d] is incompatible with our version [%d,%d]", - remoteMinVersion, remoteVersion, ProtocolMinVersion, ProtocolVersion) + + if minVersion > version { + return 0, fmt.Errorf("remote version range [%d,%d] is incompatible with ours [%d,%d]", + theirMinVersion, theirVersion, + ProtocolMinVersion, maxVersion) } - return maxVersion, nil + + if err := <-writeDone; err != nil { + return 0, err + } + + return version, nil +} + +// The V1 procotol consists of the protocol identification/version +// header, followed by a stream of gobified values. The first value +// is the encoded features map (never encrypted). The subsequent +// values are the messages on the connection (encrypted for an +// encrypted connection). For an encrypted connection, the public key +// is passed in the "PublicKey" feature as a string of hex digits. +func (res *ProtocolIntroResults) doIntroV1(params ProtocolIntroParams, pubKey, privKey *[32]byte) error { + features := filterV1Features(params.Features) + if pubKey != nil { + features["PublicKey"] = hex.EncodeToString(pubKey[:]) + } + + enc := gob.NewEncoder(params.Conn) + dec := gob.NewDecoder(params.Conn) + + // Encode in a separate goroutine to avoid the possibility of + // deadlock. The result channel is of size 1 so that the + // goroutine does not linger even if we encounter an error on + // the read side. + encodeDone := make(chan error, 1) + go func() { + encodeDone <- enc.Encode(features) + }() + + if err := dec.Decode(&res.Features); err != nil { + return err + } + + if err := <-encodeDone; err != nil { + return err + } + + if pubKey == nil { + if _, present := res.Features["PublicKey"]; present { + return ErrExpectedNoCrypto + } + + res.setupNoCrypto(enc, dec) + } else { + remotePubKeyStr, ok := res.Features["PublicKey"] + if !ok { + return ErrExpectedCrypto + } + + remotePubKey, err := hex.DecodeString(remotePubKeyStr) + if err != nil { + return err + } + + res.setupCrypto(params, enc, dec, remotePubKey, privKey) + } + + res.Features = filterV1Features(res.Features) + return nil +} + +// In the V1 protocol, the intro fields are sent unencrypted. So we +// restrict them to an established subset of fields that are assumed +// to be safe. +func filterV1Features(intro map[string]string) map[string]string { + safe := make(map[string]string) + for _, k := range ProtocolV1Features { + if val, ok := intro[k]; ok { + safe[k] = val + } + } + + return safe +} + +// The V2 procotol consists of the protocol identification/version +// header, followed by: +// +// - A single "encryption flag" byte: 0 for no encryption, 1 for +// encryption. +// +// - When the connection is encrypted, 32 bytes follow containing the +// public key. +// +// - Then a stream of gobified values. +// +// The gobified values are the messages on the connection (encrypted +// for an encrypted connection). The first message contains the +// encoded features map (so in contrast to V1, it will be encrypted on +// an encrypted connection). +func (res *ProtocolIntroResults) doIntroV2(params ProtocolIntroParams, pubKey, privKey *[32]byte) error { + // Public key exchange + var wbuf []byte + if pubKey == nil { + wbuf = []byte{0} + } else { + wbuf = make([]byte, 1+len(*pubKey)) + wbuf[0] = 1 + copy(wbuf[1:], (*pubKey)[:]) + } + + // Write in a separate goroutine to avoid the possibility of + // deadlock. The result channel is of size 1 so that the + // goroutine does not linger even if we encounter an error on + // the read side. + writeDone := make(chan error, 1) + go func() { + _, err := params.Conn.Write(wbuf) + writeDone <- err + }() + + rbuf := make([]byte, 1) + if _, err := io.ReadFull(params.Conn, rbuf); err != nil { + return err + } + + switch rbuf[0] { + case 0: + if pubKey != nil { + return ErrExpectedCrypto + } + + res.setupNoCrypto(gob.NewEncoder(params.Conn), + gob.NewDecoder(params.Conn)) + + case 1: + if pubKey == nil { + return ErrExpectedNoCrypto + } + + rbuf = make([]byte, len(pubKey)) + if _, err := io.ReadFull(params.Conn, rbuf); err != nil { + return err + } + + res.setupCrypto(params, gob.NewEncoder(params.Conn), + gob.NewDecoder(params.Conn), rbuf, privKey) + + default: + return fmt.Errorf("Bad encryption flag %d", rbuf[0]) + } + + if err := <-writeDone; err != nil { + return err + } + + // Features exchange + go func() { + buf := new(bytes.Buffer) + if err := gob.NewEncoder(buf).Encode(¶ms.Features); err != nil { + writeDone <- err + return + } + + writeDone <- res.Sender.Send(buf.Bytes()) + }() + + rbuf, err := res.Receiver.Receive() + if err != nil { + return err + } + + if err := gob.NewDecoder(bytes.NewReader(rbuf)).Decode(&res.Features); err != nil { + return err + } + + if err := <-writeDone; err != nil { + return err + } + + return nil +} + +func (res *ProtocolIntroResults) setupNoCrypto(enc *gob.Encoder, dec *gob.Decoder) { + res.Sender = NewSimpleTCPSender(enc) + res.Receiver = NewSimpleTCPReceiver(dec) +} + +func (res *ProtocolIntroResults) setupCrypto(params ProtocolIntroParams, + enc *gob.Encoder, dec *gob.Decoder, remotePubKey []byte, + privKey *[32]byte) { + var remotePubKeyArr [32]byte + copy(remotePubKeyArr[:], remotePubKey) + res.SessionKey = FormSessionKey(&remotePubKeyArr, privKey, params.Password) + res.Sender = NewEncryptedTCPSender(enc, res.SessionKey, params.Outbound) + res.Receiver = NewEncryptedTCPReceiver(dec, res.SessionKey, params.Outbound) } type ProtocolTag byte diff --git a/router/protocol_test.go b/router/protocol_test.go new file mode 100644 index 00000000..6cd36939 --- /dev/null +++ b/router/protocol_test.go @@ -0,0 +1,92 @@ +package router + +import ( + "io" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type testConn struct { + io.Writer + io.Reader +} + +func (testConn) SetDeadline(t time.Time) error { + return nil +} + +func (testConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (testConn) SetWriteDeadline(t time.Time) error { + return nil +} + +func connPair() (ProtocolIntroConn, ProtocolIntroConn) { + a := testConn{} + b := testConn{} + a.Reader, b.Writer = io.Pipe() + b.Reader, a.Writer = io.Pipe() + return &a, &b +} + +func doIntro(t *testing.T, ver byte, params ProtocolIntroParams) <-chan ProtocolIntroResults { + ch := make(chan ProtocolIntroResults, 1) + go func() { + res, err := params.doIntro(ver) + require.Nil(t, err) + ch <- res + }() + return ch +} + +func doProtocolIntro(t *testing.T, aver, bver byte, password []byte) byte { + aconn, bconn := connPair() + aresch := doIntro(t, aver, ProtocolIntroParams{ + Features: map[string]string{"Name": "A"}, + Conn: aconn, + Outbound: true, + Password: password, + }) + bresch := doIntro(t, bver, ProtocolIntroParams{ + Features: map[string]string{"Name": "B"}, + Conn: bconn, + Outbound: false, + Password: password, + }) + ares := <-aresch + bres := <-bresch + + // Check that features were conveyed + require.Equal(t, "B", ares.Features["Name"]) + require.Equal(t, "A", bres.Features["Name"]) + + // Check that Senders and Receivers work + go func() { + require.Nil(t, ares.Sender.Send([]byte("Hello from A"))) + require.Nil(t, bres.Sender.Send([]byte("Hello from B"))) + }() + + data, err := bres.Receiver.Receive() + require.Nil(t, err) + require.Equal(t, "Hello from A", string(data)) + + data, err = ares.Receiver.Receive() + require.Nil(t, err) + require.Equal(t, "Hello from B", string(data)) + + require.Equal(t, ares.Version, bres.Version) + return ares.Version +} + +func TestProtocolIntro(t *testing.T) { + require.Equal(t, 2, int(doProtocolIntro(t, 2, 2, nil))) + require.Equal(t, 2, int(doProtocolIntro(t, 2, 2, []byte("sekr1t")))) + require.Equal(t, 1, int(doProtocolIntro(t, 1, 2, nil))) + require.Equal(t, 1, int(doProtocolIntro(t, 1, 2, []byte("pa55")))) + require.Equal(t, 1, int(doProtocolIntro(t, 2, 1, nil))) + require.Equal(t, 1, int(doProtocolIntro(t, 2, 1, []byte("w0rd")))) +}