Version 2 protocol, which encrypts the features map

With V1 compatibility
This commit is contained in:
David Wragg committed 2015-07-09 14:37:42 +01:00
1 parent 501f40d9c1
commit 882f14d50c
6 files changed
+496 -212

No files matched your search

+95 -3
View File
@@ -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)
-1
View File
@@ -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
)
-148
View File
@@ -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
}
-30
View File
@@ -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")
}
+309 -30
View File
@@ -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(&params.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
+92
View File
@@ -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"))))
}