diff --git a/internal/client/auth.go b/internal/client/auth.go index 3740eb1..d26ebc3 100644 --- a/internal/client/auth.go +++ b/internal/client/auth.go @@ -8,9 +8,13 @@ import ( "sync/atomic" ) +const ( + UNORDERED_FLAG = 0x01 // 0000 0001 +) + func makeHiddenData(sta *State) (random, TLSsessionID, keyShare, sharedSecret []byte) { // random is marshalled ephemeral pub key 32 bytes - // TLSsessionID || keyShare is [encrypted UID 16 bytes, proxy method 12 bytes, encryption method 1 byte, timestamp 8 bytes, sessionID 4 bytes] [unused data] [16 bytes authentication tag] + // TLSsessionID || keyShare is [encrypted UID 16 bytes, proxy method 12 bytes, encryption method 1 byte, timestamp 8 bytes, sessionID 4 bytes] [1 byte flag] [6 bytes reserved] [16 bytes authentication tag] ephPv, ephPub, _ := ecdh.GenerateKey(rand.Reader) random = ecdh.Marshal(ephPub) @@ -21,6 +25,10 @@ func makeHiddenData(sta *State) (random, TLSsessionID, keyShare, sharedSecret [] binary.BigEndian.PutUint64(plaintext[29:37], uint64(sta.Now().Unix())) binary.BigEndian.PutUint32(plaintext[37:41], atomic.LoadUint32(&sta.SessionID)) + if sta.Unordered { + plaintext[41] |= UNORDERED_FLAG + } + sharedSecret = ecdh.GenerateSharedSecret(ephPv, sta.staticPub) nonce := random[0:12] ciphertext, _ := util.AESGCMEncrypt(nonce, sharedSecret, plaintext) diff --git a/internal/client/state.go b/internal/client/state.go index 0d44229..b5283ed 100644 --- a/internal/client/state.go +++ b/internal/client/state.go @@ -20,8 +20,7 @@ type rawConfig struct { PublicKey string BrowserSig string Unordered bool - - NumConn int + NumConn int } // State stores global variables @@ -42,6 +41,7 @@ type State struct { EncryptionMethod byte ServerName string NumConn int + Unordered bool } func InitState(localHost, localPort, remoteHost, remotePort string, nowFunc func() time.Time) *State { @@ -74,7 +74,7 @@ func ssvToJson(ssv string) (ret []byte) { value := sp[1] // JSON doesn't like quotation marks around int // Yes this is extremely ugly but it's still better than writing a tokeniser - if key == "NumConn" { + if key == "NumConn" || key == "Unordered" { ret = append(ret, []byte(`"`+key+`":`+value+`,`)...) } else { ret = append(ret, []byte(`"`+key+`":"`+value+`",`)...) @@ -125,6 +125,7 @@ func (sta *State) ParseConfig(conf string) (err error) { sta.ProxyMethod = preParse.ProxyMethod sta.ServerName = preParse.ServerName sta.NumConn = preParse.NumConn + sta.Unordered = preParse.Unordered uid, err := base64.StdEncoding.DecodeString(preParse.UID) if err != nil { diff --git a/internal/multiplex/session.go b/internal/multiplex/session.go index 210d3a4..cf4b98e 100644 --- a/internal/multiplex/session.go +++ b/internal/multiplex/session.go @@ -25,12 +25,7 @@ type Obfuscator struct { SessionKey []byte } -type SwitchboardStrategy int - -const ( - FixedConnMapping SwitchboardStrategy = iota - Uniform -) +type switchboardStrategy int type SessionConfig struct { *Obfuscator @@ -41,8 +36,6 @@ type SessionConfig struct { UnitRead func(net.Conn, []byte) (int, error) Unordered bool - - SwitchboardStrategy SwitchboardStrategy } type Session struct { @@ -84,9 +77,12 @@ func MakeSession(id uint32, config *SessionConfig) *Session { } sbConfig := &switchboardConfig{ - Valve: config.Valve, - unordered: config.Unordered, - strategy: config.SwitchboardStrategy, + Valve: config.Valve, + } + if config.Unordered { + sbConfig.strategy = UNIFORM_SPREAD + } else { + sbConfig.strategy = FIXED_CONN_MAPPING } sesh.sb = makeSwitchboard(sesh, sbConfig) go sesh.timeoutAfter(30 * time.Second) diff --git a/internal/multiplex/switchboard.go b/internal/multiplex/switchboard.go index 8a98a30..bec8c5f 100644 --- a/internal/multiplex/switchboard.go +++ b/internal/multiplex/switchboard.go @@ -9,10 +9,14 @@ import ( "sync/atomic" ) +const ( + FIXED_CONN_MAPPING switchboardStrategy = iota + UNIFORM_SPREAD +) + type switchboardConfig struct { Valve - unordered bool - strategy SwitchboardStrategy + strategy switchboardStrategy } // switchboard is responsible for keeping the reference of TLS connections between client and server @@ -64,34 +68,39 @@ func (sb *switchboard) removeConn(connId uint32) { // a pointer to connId is passed here so that the switchboard can reassign it func (sb *switchboard) send(data []byte, connId *uint32) (int, error) { - sb.Valve.rxWait(len(data)) sb.connsM.RLock() defer sb.connsM.RUnlock() - var conn net.Conn - conn, ok := sb.conns[*connId] - if ok { - n, err := conn.Write(data) - sb.Valve.AddTx(int64(n)) - return n, err - } else { - // do not call assignRandomConn() here. - // we'll have to do connsM.RLock() after we get a new connId from assignRandomConn, in order to - // get the new conn through conns[newConnId] - // however between connsM.RUnlock() in assignRandomConn and our call to connsM.RLock(), things may happen. - // in particular if newConnId is removed between the RUnlock and RLock, conns[newConnId] will return - // a nil pointer. To prevent this we must get newConnId and the reference to conn itself in one single mutex - // protection - if atomic.LoadUint32(&sb.broken) == 1 || len(sb.conns) == 0 { - return 0, errBrokenSwitchboard - } - newConnId := rand.Intn(len(sb.conns)) - conn, ok = sb.conns[uint32(newConnId)] + if sb.strategy == UNIFORM_SPREAD { + randConnId := rand.Intn(len(sb.conns)) + conn, ok := sb.conns[uint32(randConnId)] if !ok { return 0, errBrokenSwitchboard } else { - n, err := conn.Write(data) - sb.Valve.AddTx(int64(n)) - return n, err + return conn.Write(data) + } + } else { + var conn net.Conn + conn, ok := sb.conns[*connId] + if ok { + return conn.Write(data) + } else { + // do not call assignRandomConn() here. + // we'll have to do connsM.RLock() after we get a new connId from assignRandomConn, in order to + // get the new conn through conns[newConnId] + // however between connsM.RUnlock() in assignRandomConn and our call to connsM.RLock(), things may happen. + // in particular if newConnId is removed between the RUnlock and RLock, conns[newConnId] will return + // a nil pointer. To prevent this we must get newConnId and the reference to conn itself in one single mutex + // protection + if atomic.LoadUint32(&sb.broken) == 1 || len(sb.conns) == 0 { + return 0, errBrokenSwitchboard + } + newConnId := rand.Intn(len(sb.conns)) + conn, ok = sb.conns[uint32(newConnId)] + if !ok { + return 0, errBrokenSwitchboard + } else { + return conn.Write(data) + } } } diff --git a/internal/multiplex/switchboard_test.go b/internal/multiplex/switchboard_test.go index 89e410f..756cc83 100644 --- a/internal/multiplex/switchboard_test.go +++ b/internal/multiplex/switchboard_test.go @@ -1,7 +1,6 @@ package multiplex import ( - "github.com/cbeuw/Cloak/internal/util" "math/rand" "testing" ) @@ -10,13 +9,17 @@ func BenchmarkSwitchboard_Send(b *testing.B) { seshConfig := &SessionConfig{ Obfuscator: nil, Valve: nil, - UnitRead: util.ReadTLS, + UnitRead: nil, } sesh := MakeSession(0, seshConfig) - + sbConfig := &switchboardConfig{ + Valve: UNLIMITED_VALVE, + strategy: FIXED_CONN_MAPPING, + } + sb := makeSwitchboard(sesh, sbConfig) hole := newBlackHole() - sesh.sb.addConn(hole) - connId, err := sesh.sb.assignRandomConn() + sb.addConn(hole) + connId, err := sb.assignRandomConn() if err != nil { b.Error("failed to get a random conn", err) return @@ -25,7 +28,7 @@ func BenchmarkSwitchboard_Send(b *testing.B) { rand.Read(data) b.ResetTimer() for i := 0; i < b.N; i++ { - n, err := sesh.sb.send(data, &connId) + n, err := sb.send(data, &connId) if err != nil { b.Error(err) return @@ -33,34 +36,3 @@ func BenchmarkSwitchboard_Send(b *testing.B) { b.SetBytes(int64(n)) } } - -func TestSwitchboard_TxCredit(t *testing.T) { - seshConfig := &SessionConfig{ - Obfuscator: nil, - Valve: MakeValve(1<<20, 1<<20), - UnitRead: util.ReadTLS, - } - sesh := MakeSession(0, seshConfig) - hole := newBlackHole() - sesh.sb.addConn(hole) - connId, err := sesh.sb.assignRandomConn() - if err != nil { - t.Error("failed to get a random conn", err) - return - } - data := make([]byte, 1000) - rand.Read(data) - - n, err := sesh.sb.send(data[:10], &connId) - if err != nil { - t.Error(err) - return - } - if n != 10 { - t.Errorf("wanted to send %v, got %v", 10, n) - return - } - if *sesh.sb.Valve.(*LimitedValve).tx != 10 { - t.Error("tx credit didn't increase by 10") - } -} diff --git a/internal/server/auth.go b/internal/server/auth.go index 761dd7a..e7dbb3d 100644 --- a/internal/server/auth.go +++ b/internal/server/auth.go @@ -15,8 +15,13 @@ type ClientInfo struct { SessionId uint32 ProxyMethod string EncryptionMethod byte + Unordered bool } +const ( + UNORDERED_FLAG = 0x01 // 0000 0001 +) + var ErrReplay = errors.New("duplicate random") var ErrInvalidPubKey = errors.New("public key has invalid format") var ErrCiphertextLength = errors.New("ciphertext has the wrong length") @@ -59,6 +64,7 @@ func TouchStone(ch *ClientHello, sta *State) (info ClientInfo, sharedSecret []by SessionId: 0, ProxyMethod: string(bytes.Trim(plaintext[16:28], "\x00")), EncryptionMethod: plaintext[28], + Unordered: plaintext[41]&UNORDERED_FLAG != 0, } timestamp := int64(binary.BigEndian.Uint64(plaintext[29:37]))