More comments

pull/71/head
Andy Wang 5 years ago
parent c44a061cbe
commit 87a7684e10

@ -37,6 +37,8 @@ func addExtRec(typ []byte, data []byte) []byte {
return ret
}
// PrepareConnection handles the TLS handshake for a given conn and returns the sessionKey
// if the server proceed with Cloak authentication
func PrepareConnection(sta *State, conn net.Conn) (sessionKey []byte, err error) {
hd, sharedSecret := makeHiddenData(sta)
chOnly := sta.browser.composeClientHello(hd)
@ -56,8 +58,8 @@ func PrepareConnection(sta *State, conn net.Conn) (sessionKey []byte, err error)
encrypted := append(buf[11:43], buf[89:121]...)
nonce := encrypted[0:12]
ciphertext := encrypted[12:60]
sessionKey, err = util.AESGCMDecrypt(nonce, sharedSecret, ciphertext)
ciphertextWithTag := encrypted[12:60]
sessionKey, err = util.AESGCMDecrypt(nonce, sharedSecret, ciphertextWithTag)
if err != nil {
return
}

@ -19,9 +19,19 @@ type chHiddenData struct {
chExtSNI []byte
}
// makeHiddenData generates the ephemeral key pair, calculates the shared secret, and then compose and
// encrypt the Authentication data. It also composes SNI extension.
func makeHiddenData(sta *State) (ret chHiddenData, 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] [1 byte flag] [6 bytes reserved] [16 bytes authentication tag]
/*
Authentication data:
+----------+----------------+---------------------+-------------+--------------+--------+------------+
| _UID_ | _Proxy Method_ | _Encryption Method_ | _Timestamp_ | _Session Id_ | _Flag_ | _reserved_ |
+----------+----------------+---------------------+-------------+--------------+--------+------------+
| 16 bytes | 12 bytes | 1 byte | 8 bytes | 4 bytes | 1 byte | 6 bytes |
+----------+----------------+---------------------+-------------+--------------+--------+------------+
*/
// The authentication ciphertext and its tag are then distributed among SessionId and X25519KeyShare
ephPv, ephPub, _ := ecdh.GenerateKey(rand.Reader)
ret.chRandom = ecdh.Marshal(ephPub)
@ -38,9 +48,9 @@ func makeHiddenData(sta *State) (ret chHiddenData, sharedSecret []byte) {
sharedSecret = ecdh.GenerateSharedSecret(ephPv, sta.staticPub)
nonce := ret.chRandom[0:12]
ciphertext, _ := util.AESGCMEncrypt(nonce, sharedSecret, plaintext)
ret.chSessionId = ciphertext[0:32]
ret.chX25519KeyShare = ciphertext[32:64]
ciphertextWithTag, _ := util.AESGCMEncrypt(nonce, sharedSecret, plaintext)
ret.chSessionId = ciphertextWithTag[0:32]
ret.chX25519KeyShare = ciphertextWithTag[32:64]
ret.chExtSNI = makeServerName(sta.ServerName)
return
}

@ -1,4 +1,4 @@
// Chrome 76
// Fingerprint of Chrome 76
package client

@ -1,4 +1,5 @@
// Firefox 68
// Fingerprint of Firefox 68
package client
import (

@ -10,8 +10,10 @@ import (
"time"
"github.com/cbeuw/Cloak/internal/ecdh"
mux "github.com/cbeuw/Cloak/internal/multiplex"
)
// rawConfig represents the fields in the config json file
type rawConfig struct {
ServerName string
ProxyMethod string
@ -23,7 +25,7 @@ type rawConfig struct {
StreamTimeout int
}
// State stores global variables
// State stores the parsed configuration fields
type State struct {
LocalHost string
LocalPort string
@ -35,7 +37,7 @@ type State struct {
UID []byte
staticPub crypto.PublicKey
now func() time.Time
now func() time.Time // for easier testing
browser browser
ProxyMethod string
@ -45,6 +47,7 @@ type State struct {
Timeout time.Duration
}
// TODO: remove this and let the caller declare it directly
func InitState(localHost, localPort, remoteHost, remotePort string, nowFunc func() time.Time) *State {
ret := &State{
LocalHost: localHost,
@ -73,8 +76,8 @@ func ssvToJson(ssv string) (ret []byte) {
sp := strings.SplitN(ln, "=", 2)
key := sp[0]
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
// JSON doesn't like quotation marks around int and bool
// This is extremely ugly but it's still better than writing a tokeniser
if key == "NumConn" || key == "Unordered" || key == "StreamTimeout" {
ret = append(ret, []byte(`"`+key+`":`+value+`,`)...)
} else {
@ -89,6 +92,7 @@ func ssvToJson(ssv string) (ret []byte) {
// ParseConfig parses the config (either a path to json or Android config) into a State variable
func (sta *State) ParseConfig(conf string) (err error) {
var content []byte
// Checking if it's a path to json or a ssv string
if strings.Contains(conf, ";") && strings.Contains(conf, "=") {
content = ssvToJson(conf)
} else {
@ -105,11 +109,11 @@ func (sta *State) ParseConfig(conf string) (err error) {
switch strings.ToLower(preParse.EncryptionMethod) {
case "plain":
sta.EncryptionMethod = 0x00
sta.EncryptionMethod = mux.E_METHOD_PLAIN
case "aes-gcm":
sta.EncryptionMethod = 0x01
sta.EncryptionMethod = mux.E_METHOD_AES_GCM
case "chacha20-poly1305":
sta.EncryptionMethod = 0x02
sta.EncryptionMethod = mux.E_METHOD_CHACHA20_POLY1305
default:
return errors.New("Unknown encryption method")
}

@ -10,6 +10,7 @@ import (
const BUF_SIZE_LIMIT = 1 << 20 * 500
// The point of a bufferedPipe is that Read() will block until data is available
type bufferedPipe struct {
buf *bytes.Buffer
closed bool

@ -9,6 +9,9 @@ import (
const DATAGRAM_NUMBER_LIMIT = 1024
// datagramBuffer is the same as bufferedPipe with the exception that it's message-oriented,
// instead of byte-oriented. The integrity of datagrams written into this buffer is preserved.
// it won't get chopped up into individual bytes
type datagramBuffer struct {
buf [][]byte
closed bool
@ -36,6 +39,7 @@ func (d *datagramBuffer) Read(target []byte) (int, error) {
}
d.rwCond.Wait()
}
// TODO: return error if len(target) is smaller than the datagram
var data []byte
data, d.buf = d.buf[0], d.buf[1:]
copy(target, data)

@ -28,6 +28,9 @@ const (
func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
obfs := func(f *Frame, buf []byte) (int, error) {
// we need the encrypted data to be at least 8 bytes to be used as nonce for salsa20 stream header encryption
// this will be the case if the encryption method is an AEAD cipher, however for plain, it's well possible
// that the frame payload is smaller than 8 bytes, so we need to add on the difference
var extraLen uint8
if payloadCipher == nil {
if len(f.Payload) < 8 {
@ -37,14 +40,29 @@ func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
extraLen = uint8(payloadCipher.Overhead())
}
// usefulLen is the amount of bytes that will be eventually sent off
usefulLen := 5 + HEADER_LEN + len(f.Payload) + int(extraLen)
if len(buf) < usefulLen {
return 0, errors.New("buffer is too small")
}
// we do as much in-place as possible to save allocation
useful := buf[:usefulLen] // tls header + payload + potential overhead
recordLayer := useful[0:5]
header := useful[5 : 5+HEADER_LEN]
encryptedPayload := useful[5+HEADER_LEN:]
encryptedPayloadWithExtra := useful[5+HEADER_LEN:]
// TODO: Once Seq wraps around, the chance of a nonce reuse will be 1/65536 which is unacceptably low
// prohibit Seq wrap around? simple solution : 2^32 messages per stream may be too little
//
// use uint64 Seq? Vastly reduces the complexity of frameSorter : concern with 64 bit number performance on
// embedded systems (frameSorter already has a non-trivial performance impact on RPi2B, can only be worse on
// mipsle). HOWEVER since frameSorter already deals with uint64, prehaps changing it totally wouldn't matter much?
//
// regular rekey? Improves security in general : when to rekey? Not easy to synchronise, also will add a decent
// amount of complexity
//
// LEANING TOWARDS uint64 Seq. Adds extra 2 bytes of overhead but shouldn't really matter that much
// header: [StreamID 4 bytes][Seq 4 bytes][Closing 1 byte][extraLen 1 bytes][random 2 bytes]
putU32(header[0:4], f.StreamID)
@ -54,16 +72,16 @@ func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
prand.Read(header[10:12])
if payloadCipher == nil {
copy(encryptedPayload, f.Payload)
copy(encryptedPayloadWithExtra, f.Payload)
if extraLen != 0 {
rand.Read(encryptedPayload[len(encryptedPayload)-int(extraLen):])
rand.Read(encryptedPayloadWithExtra[len(encryptedPayloadWithExtra)-int(extraLen):])
}
} else {
ciphertext := payloadCipher.Seal(nil, header, f.Payload, nil)
copy(encryptedPayload, ciphertext)
copy(encryptedPayloadWithExtra, ciphertext)
}
nonce := encryptedPayload[len(encryptedPayload)-8:]
nonce := encryptedPayloadWithExtra[len(encryptedPayloadWithExtra)-8:]
salsa20.XORKeyStream(header, header, nonce, &salsaKey)
// Composing final obfsed message
@ -71,7 +89,7 @@ func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
recordLayer[0] = 0x17
recordLayer[1] = 0x03
recordLayer[2] = 0x03
binary.BigEndian.PutUint16(recordLayer[3:5], uint16(HEADER_LEN+len(encryptedPayload)))
binary.BigEndian.PutUint16(recordLayer[3:5], uint16(HEADER_LEN+len(encryptedPayloadWithExtra)))
return usefulLen, nil
}
return obfs
@ -87,7 +105,7 @@ func MakeDeobfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Deobfser {
copy(peeled, in[5:])
header := peeled[:12]
pldWithOverHead := peeled[12:] // plaintext + potential overhead
pldWithOverHead := peeled[12:] // payload + potential overhead
nonce := peeled[len(peeled)-8:]
salsa20.XORKeyStream(header, header, nonce, &salsaKey)

@ -18,6 +18,7 @@ const (
var ErrBrokenSession = errors.New("broken session")
var errRepeatSessionClosing = errors.New("trying to close a closed session")
// Obfuscator is responsible for the obfuscation and deobfuscation of frames
type Obfuscator struct {
// Used in Stream.Write. Add multiplexing headers, encrypt and add TLS header
Obfs Obfser
@ -33,7 +34,7 @@ type SessionConfig struct {
Valve
// This is supposed to read one TLS message, the same as GoQuiet's ReadTillDrain
// This is supposed to read one TLS message.
UnitRead func(net.Conn, []byte) (int, error)
Unordered bool

@ -15,6 +15,8 @@ import (
var ErrBrokenStream = errors.New("broken stream")
// ReadWriteCloseLener is io.ReadWriteCloser with Len()
// used for bufferedPipe and datagramBuffer
type ReadWriteCloseLener interface {
io.ReadWriteCloser
Len() int
@ -34,6 +36,7 @@ type Stream struct {
writingM sync.RWMutex
// atomic
closed uint32
obfsBuf []byte
@ -41,6 +44,7 @@ type Stream struct {
// we assign each stream a fixed underlying TCP connection to utilise order guarantee provided by TCP itself
// so that frameSorter should have few to none ooo frames to deal with
// overall the streams in a session should be uniformly distributed across all connections
// This is not used in unordered connection mode
assignedConnId uint32
}
@ -64,8 +68,6 @@ func makeStream(sesh *Session, id uint32, assignedConnId uint32) *Stream {
return stream
}
//func (s *Stream) reassignConnId(connId uint32) { atomic.StoreUint32(&s.assignedConnId,connId)}
func (s *Stream) isClosed() bool { return atomic.LoadUint32(&s.closed) == 1 }
func (s *Stream) writeFrame(frame *Frame) {
@ -77,6 +79,7 @@ func (s *Stream) writeFrame(frame *Frame) {
}
}
// Read implements io.Read
func (s *Stream) Read(buf []byte) (n int, err error) {
//log.Tracef("attempting to read from stream %v", s.id)
if len(buf) == 0 {
@ -103,6 +106,7 @@ func (s *Stream) Read(buf []byte) (n int, err error) {
}
}
// Write implements io.Write
func (s *Stream) Write(in []byte) (n int, err error) {
// RWMutex used here isn't really for RW.
// we use it to exploit the fact that RLock doesn't create contention.

@ -113,6 +113,7 @@ func (sb *switchboard) send(data []byte, connId *uint32) (n int, err error) {
}
// returns a random connId
func (sb *switchboard) assignRandomConn() (uint32, error) {
sb.connsM.RLock()
defer sb.connsM.RUnlock()
@ -124,6 +125,7 @@ func (sb *switchboard) assignRandomConn() (uint32, error) {
}
// actively triggered by session.Close()
// TODO: closeALl needs to clear the conns map
func (sb *switchboard) closeAll() {
if atomic.SwapUint32(&sb.broken, 1) == 1 {
return

@ -199,9 +199,8 @@ func composeServerHello(sessionId []byte, sharedSecret []byte, sessionKey []byte
return ret, nil
}
// composeReply composes the ServerHello, ChangeCipherSpec and Finished messages
// together with their respective record layers into one byte slice. The content
// of these messages are random and useless for this plugin
// composeReply composes the ServerHello, ChangeCipherSpec and an ApplicationData messages
// together with their respective record layers into one byte slice.
func composeReply(ch *ClientHello, sharedSecret []byte, sessionKey []byte) ([]byte, error) {
TLS12 := []byte{0x03, 0x03}
sh, err := composeServerHello(ch.sessionId, sharedSecret, sessionKey)
@ -223,6 +222,10 @@ var ErrNotCloak = errors.New("TLS but non-Cloak ClientHello")
var ErrReplay = errors.New("duplicate random")
var ErrBadProxyMethod = errors.New("invalid proxy method")
// PrepareConnection checks if the first packet of data is ClientHello, and checks if it was from a Cloak client
// if it is from a Cloak client, it returns the ClientInfo with the decrypted fields. It doesn't check if the user
// is authorised. It also returns a finisher callback function to be called when the caller wishes to proceed with
// the handshake
func PrepareConnection(firstPacket []byte, sta *State, conn net.Conn) (info ClientInfo, finisher func([]byte) error, err error) {
ch, err := parseClientHello(firstPacket)
if err != nil {

@ -20,6 +20,7 @@ type ActiveUser struct {
sessions map[uint32]*mux.Session
}
// DeleteSession closes a session and removes its reference from the user
func (u *ActiveUser) DeleteSession(sessionID uint32, reason string) {
u.sessionsM.Lock()
sesh, existing := u.sessions[sessionID]
@ -34,6 +35,9 @@ func (u *ActiveUser) DeleteSession(sessionID uint32, reason string) {
u.sessionsM.Unlock()
}
// GetSession returns the reference to an existing session, or if one such session doesn't exist, it queries
// the UserManager for the authorisation for a new session. If a new session is allowed, it creates this new session
// and returns its reference
func (u *ActiveUser) GetSession(sessionID uint32, config *mux.SessionConfig) (sesh *mux.Session, existing bool, err error) {
u.sessionsM.Lock()
defer u.sessionsM.Unlock()
@ -54,6 +58,7 @@ func (u *ActiveUser) GetSession(sessionID uint32, config *mux.SessionConfig) (se
}
}
// Terminate closes all sessions of this active user
func (u *ActiveUser) Terminate(reason string) {
u.sessionsM.Lock()
for _, sesh := range u.sessions {
@ -66,6 +71,7 @@ func (u *ActiveUser) Terminate(reason string) {
u.panel.DeleteActiveUser(u)
}
// NumSession returns the number of active sessions
func (u *ActiveUser) NumSession() int {
u.sessionsM.RLock()
defer u.sessionsM.RUnlock()

@ -27,6 +27,8 @@ var ErrInvalidPubKey = errors.New("public key has invalid format")
var ErrCiphertextLength = errors.New("ciphertext has the wrong length")
var ErrTimestampOutOfWindow = errors.New("timestamp is outside of the accepting window")
// touchStone checks if a ClientHello came from a Cloak client by checking and decrypting the fields Cloak hides data in
// It returns the ClientInfo, but it doesn't check if the UID is authorised
func touchStone(ch *ClientHello, staticPv crypto.PrivateKey, now func() time.Time) (info ClientInfo, sharedSecret []byte, err error) {
ephPub, ok := ecdh.Unmarshal(ch.random)
if !ok {

@ -63,7 +63,7 @@ func InitState(bindHost, bindPort string, nowFunc func() time.Time) (*State, err
return ret, nil
}
// ParseConfig parses the config (either a path to json or in-line ssv config) into a State variable
// ParseConfig parses the config (either a path to json or the json itself as argument) into a State variable
func (sta *State) ParseConfig(conf string) (err error) {
var content []byte
var preParse rawConfig
@ -83,7 +83,6 @@ func (sta *State) ParseConfig(conf string) (err error) {
if preParse.CncMode {
//TODO: implement command & control mode
} else {
manager, err := usermanager.MakeLocalManager(preParse.DatabasePath)
if err != nil {
@ -143,6 +142,7 @@ func (sta *State) ParseConfig(conf string) (err error) {
return nil
}
// IsBypass checks if a UID is a bypass user
func (sta *State) IsBypass(UID []byte) bool {
var arrUID [16]byte
copy(arrUID[:], UID)
@ -154,7 +154,7 @@ const TIMESTAMP_TOLERANCE = 180 * time.Second
const CACHE_CLEAN_INTERVAL = 12 * time.Hour
// UsedRandomCleaner clears the cache of used random fields every 12 hours
// UsedRandomCleaner clears the cache of used random fields every CACHE_CLEAN_INTERVAL
func (sta *State) UsedRandomCleaner() {
for {
time.Sleep(CACHE_CLEAN_INTERVAL)

@ -26,6 +26,7 @@ func i32ToB(value int32) []byte {
return nib
}
// localManager is responsible for routing API calls to appropriate handlers and manage the local user database accordingly
type localManager struct {
db *bolt.DB
Router *gmux.Router
@ -63,6 +64,8 @@ func (manager *localManager) registerMux() *gmux.Router {
return r
}
// Authenticate user returns err==nil along with the users' up and down bandwidths if the UID is allowed to connect
// More specifically it checks that the user exists, that it has positive credit and that it hasn't expired
func (manager *localManager) AuthenticateUser(UID []byte) (int64, int64, error) {
var upRate, downRate, upCredit, downCredit, expiryTime int64
err := manager.db.View(func(tx *bolt.Tx) error {
@ -93,6 +96,8 @@ func (manager *localManager) AuthenticateUser(UID []byte) (int64, int64, error)
return upRate, downRate, nil
}
// AuthoriseNewSession returns err==nil when the user is allowed to make a new session
// More specifically it checks that the user exists, has credit, hasn't expired and hasn't reached sessionsCap
func (manager *localManager) AuthoriseNewSession(UID []byte, ainfo AuthorisationInfo) error {
var arrUID [16]byte
copy(arrUID[:], UID)
@ -128,6 +133,9 @@ func (manager *localManager) AuthoriseNewSession(UID []byte, ainfo Authorisation
return nil
}
// UploadStatus gets StatusUpdates representing the recent status of each user, and update them in the database
// it returns a slice of StatusResponse, which represents actions need to be taken for specific users.
// If no action is needed, there won't be a StatusResponse entry for that user
func (manager *localManager) UploadStatus(uploads []StatusUpdate) ([]StatusResponse, error) {
var responses []StatusResponse
err := manager.db.Update(func(tx *bolt.Tx) error {

@ -29,6 +29,7 @@ func MakeUserPanel(manager usermanager.UserManager) *userPanel {
return ret
}
// GetBypassUser does the same as GetUser except it unconditionally creates an ActiveUser when the UID isn't already active
func (panel *userPanel) GetBypassUser(UID []byte) (*ActiveUser, error) {
panel.activeUsersM.Lock()
var arrUID [16]byte
@ -49,6 +50,8 @@ func (panel *userPanel) GetBypassUser(UID []byte) (*ActiveUser, error) {
return user, nil
}
// GetUser retrieves the reference to an ActiveUser if it's already active, or creates a new ActiveUser of specified
// UID with UserInfo queried from the UserManger, should the particular UID is allowed to connect
func (panel *userPanel) GetUser(UID []byte) (*ActiveUser, error) {
panel.activeUsersM.Lock()
var arrUID [16]byte
@ -76,7 +79,9 @@ func (panel *userPanel) GetUser(UID []byte) (*ActiveUser, error) {
return user, nil
}
// DeleteActiveUser deletes the references to the active user
func (panel *userPanel) DeleteActiveUser(user *ActiveUser) {
// TODO: terminate the user here?
panel.updateUsageQueueForOne(user)
panel.activeUsersM.Lock()
delete(panel.activeUsers, user.arrUID)
@ -97,6 +102,7 @@ type usagePair struct {
down *int64
}
// updateUsageQueue zeroes the accumulated usage all ActiveUsers valve and put the usage data im usageUpdateQueue
func (panel *userPanel) updateUsageQueue() {
panel.activeUsersM.Lock()
panel.usageUpdateQueueM.Lock()
@ -119,6 +125,8 @@ func (panel *userPanel) updateUsageQueue() {
panel.usageUpdateQueueM.Unlock()
}
// updateUsageQueueForOne is the same as updateUsageQueue except it only updates one user's usage
// this is useful when the user is being terminated
func (panel *userPanel) updateUsageQueueForOne(user *ActiveUser) {
// used when one particular user deactivates
if user.bypass {
@ -137,6 +145,8 @@ func (panel *userPanel) updateUsageQueueForOne(user *ActiveUser) {
}
// commitUpdate put all usageUpdates into a slice of StatusUpdate, calls Manager.UploadStatus, gets the responses
// and act to each user according to the responses
func (panel *userPanel) commitUpdate() error {
panel.usageUpdateQueueM.Lock()
statuses := make([]usermanager.StatusUpdate, 0, len(panel.usageUpdateQueue))

@ -58,11 +58,14 @@ func ReadTLS(conn net.Conn, buffer []byte) (n int, err error) {
left := dataLength
readPtr := 5
// TODO: Deadline here?
for left != 0 {
// If left > buffer size (i.e. our message got segmented), the entire MTU is read
// if left = buffer size, the entire buffer is all there left to read
// if left < buffer size (i.e. multiple messages came together),
// only the message we want is read
// TODO: Why ReadFull here? Shouldn't it be just normal read since we adjust left and readPtr according to read amount?
i, err = io.ReadFull(conn, buffer[readPtr:readPtr+left])
if err != nil {
return

Loading…
Cancel
Save