Version 2 protocol, which encrypts the features map
With V1 compatibility
This commit is contained in:
1 parent
501f40d9c1
commit
882f14d50c
6 files changed
+496
-212
No files matched your search
+95
-3
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"))))
|
||||
}
|
||||
Reference in new issue
Block a user