mirror of https://github.com/cbeuw/Cloak
More comments
This commit is contained in:
parent
c44a061cbe
commit
87a7684e10
|
|
@ -37,6 +37,8 @@ func addExtRec(typ []byte, data []byte) []byte {
|
||||||
return ret
|
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) {
|
func PrepareConnection(sta *State, conn net.Conn) (sessionKey []byte, err error) {
|
||||||
hd, sharedSecret := makeHiddenData(sta)
|
hd, sharedSecret := makeHiddenData(sta)
|
||||||
chOnly := sta.browser.composeClientHello(hd)
|
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]...)
|
encrypted := append(buf[11:43], buf[89:121]...)
|
||||||
nonce := encrypted[0:12]
|
nonce := encrypted[0:12]
|
||||||
ciphertext := encrypted[12:60]
|
ciphertextWithTag := encrypted[12:60]
|
||||||
sessionKey, err = util.AESGCMDecrypt(nonce, sharedSecret, ciphertext)
|
sessionKey, err = util.AESGCMDecrypt(nonce, sharedSecret, ciphertextWithTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,19 @@ type chHiddenData struct {
|
||||||
chExtSNI []byte
|
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) {
|
func makeHiddenData(sta *State) (ret chHiddenData, sharedSecret []byte) {
|
||||||
// random is marshalled ephemeral pub key 32 bytes
|
// 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)
|
ephPv, ephPub, _ := ecdh.GenerateKey(rand.Reader)
|
||||||
ret.chRandom = ecdh.Marshal(ephPub)
|
ret.chRandom = ecdh.Marshal(ephPub)
|
||||||
|
|
||||||
|
|
@ -38,9 +48,9 @@ func makeHiddenData(sta *State) (ret chHiddenData, sharedSecret []byte) {
|
||||||
|
|
||||||
sharedSecret = ecdh.GenerateSharedSecret(ephPv, sta.staticPub)
|
sharedSecret = ecdh.GenerateSharedSecret(ephPv, sta.staticPub)
|
||||||
nonce := ret.chRandom[0:12]
|
nonce := ret.chRandom[0:12]
|
||||||
ciphertext, _ := util.AESGCMEncrypt(nonce, sharedSecret, plaintext)
|
ciphertextWithTag, _ := util.AESGCMEncrypt(nonce, sharedSecret, plaintext)
|
||||||
ret.chSessionId = ciphertext[0:32]
|
ret.chSessionId = ciphertextWithTag[0:32]
|
||||||
ret.chX25519KeyShare = ciphertext[32:64]
|
ret.chX25519KeyShare = ciphertextWithTag[32:64]
|
||||||
ret.chExtSNI = makeServerName(sta.ServerName)
|
ret.chExtSNI = makeServerName(sta.ServerName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Chrome 76
|
// Fingerprint of Chrome 76
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// Firefox 68
|
// Fingerprint of Firefox 68
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
"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 {
|
type rawConfig struct {
|
||||||
ServerName string
|
ServerName string
|
||||||
ProxyMethod string
|
ProxyMethod string
|
||||||
|
|
@ -23,7 +25,7 @@ type rawConfig struct {
|
||||||
StreamTimeout int
|
StreamTimeout int
|
||||||
}
|
}
|
||||||
|
|
||||||
// State stores global variables
|
// State stores the parsed configuration fields
|
||||||
type State struct {
|
type State struct {
|
||||||
LocalHost string
|
LocalHost string
|
||||||
LocalPort string
|
LocalPort string
|
||||||
|
|
@ -35,7 +37,7 @@ type State struct {
|
||||||
UID []byte
|
UID []byte
|
||||||
|
|
||||||
staticPub crypto.PublicKey
|
staticPub crypto.PublicKey
|
||||||
now func() time.Time
|
now func() time.Time // for easier testing
|
||||||
browser browser
|
browser browser
|
||||||
|
|
||||||
ProxyMethod string
|
ProxyMethod string
|
||||||
|
|
@ -45,6 +47,7 @@ type State struct {
|
||||||
Timeout time.Duration
|
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 {
|
func InitState(localHost, localPort, remoteHost, remotePort string, nowFunc func() time.Time) *State {
|
||||||
ret := &State{
|
ret := &State{
|
||||||
LocalHost: localHost,
|
LocalHost: localHost,
|
||||||
|
|
@ -73,8 +76,8 @@ func ssvToJson(ssv string) (ret []byte) {
|
||||||
sp := strings.SplitN(ln, "=", 2)
|
sp := strings.SplitN(ln, "=", 2)
|
||||||
key := sp[0]
|
key := sp[0]
|
||||||
value := sp[1]
|
value := sp[1]
|
||||||
// JSON doesn't like quotation marks around int
|
// JSON doesn't like quotation marks around int and bool
|
||||||
// Yes this is extremely ugly but it's still better than writing a tokeniser
|
// This is extremely ugly but it's still better than writing a tokeniser
|
||||||
if key == "NumConn" || key == "Unordered" || key == "StreamTimeout" {
|
if key == "NumConn" || key == "Unordered" || key == "StreamTimeout" {
|
||||||
ret = append(ret, []byte(`"`+key+`":`+value+`,`)...)
|
ret = append(ret, []byte(`"`+key+`":`+value+`,`)...)
|
||||||
} else {
|
} 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
|
// ParseConfig parses the config (either a path to json or Android config) into a State variable
|
||||||
func (sta *State) ParseConfig(conf string) (err error) {
|
func (sta *State) ParseConfig(conf string) (err error) {
|
||||||
var content []byte
|
var content []byte
|
||||||
|
// Checking if it's a path to json or a ssv string
|
||||||
if strings.Contains(conf, ";") && strings.Contains(conf, "=") {
|
if strings.Contains(conf, ";") && strings.Contains(conf, "=") {
|
||||||
content = ssvToJson(conf)
|
content = ssvToJson(conf)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -105,11 +109,11 @@ func (sta *State) ParseConfig(conf string) (err error) {
|
||||||
|
|
||||||
switch strings.ToLower(preParse.EncryptionMethod) {
|
switch strings.ToLower(preParse.EncryptionMethod) {
|
||||||
case "plain":
|
case "plain":
|
||||||
sta.EncryptionMethod = 0x00
|
sta.EncryptionMethod = mux.E_METHOD_PLAIN
|
||||||
case "aes-gcm":
|
case "aes-gcm":
|
||||||
sta.EncryptionMethod = 0x01
|
sta.EncryptionMethod = mux.E_METHOD_AES_GCM
|
||||||
case "chacha20-poly1305":
|
case "chacha20-poly1305":
|
||||||
sta.EncryptionMethod = 0x02
|
sta.EncryptionMethod = mux.E_METHOD_CHACHA20_POLY1305
|
||||||
default:
|
default:
|
||||||
return errors.New("Unknown encryption method")
|
return errors.New("Unknown encryption method")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
const BUF_SIZE_LIMIT = 1 << 20 * 500
|
const BUF_SIZE_LIMIT = 1 << 20 * 500
|
||||||
|
|
||||||
|
// The point of a bufferedPipe is that Read() will block until data is available
|
||||||
type bufferedPipe struct {
|
type bufferedPipe struct {
|
||||||
buf *bytes.Buffer
|
buf *bytes.Buffer
|
||||||
closed bool
|
closed bool
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ import (
|
||||||
|
|
||||||
const DATAGRAM_NUMBER_LIMIT = 1024
|
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 {
|
type datagramBuffer struct {
|
||||||
buf [][]byte
|
buf [][]byte
|
||||||
closed bool
|
closed bool
|
||||||
|
|
@ -36,6 +39,7 @@ func (d *datagramBuffer) Read(target []byte) (int, error) {
|
||||||
}
|
}
|
||||||
d.rwCond.Wait()
|
d.rwCond.Wait()
|
||||||
}
|
}
|
||||||
|
// TODO: return error if len(target) is smaller than the datagram
|
||||||
var data []byte
|
var data []byte
|
||||||
data, d.buf = d.buf[0], d.buf[1:]
|
data, d.buf = d.buf[0], d.buf[1:]
|
||||||
copy(target, data)
|
copy(target, data)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ const (
|
||||||
|
|
||||||
func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
|
func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
|
||||||
obfs := func(f *Frame, buf []byte) (int, error) {
|
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
|
var extraLen uint8
|
||||||
if payloadCipher == nil {
|
if payloadCipher == nil {
|
||||||
if len(f.Payload) < 8 {
|
if len(f.Payload) < 8 {
|
||||||
|
|
@ -37,14 +40,29 @@ func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
|
||||||
extraLen = uint8(payloadCipher.Overhead())
|
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)
|
usefulLen := 5 + HEADER_LEN + len(f.Payload) + int(extraLen)
|
||||||
if len(buf) < usefulLen {
|
if len(buf) < usefulLen {
|
||||||
return 0, errors.New("buffer is too small")
|
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
|
useful := buf[:usefulLen] // tls header + payload + potential overhead
|
||||||
recordLayer := useful[0:5]
|
recordLayer := useful[0:5]
|
||||||
header := useful[5 : 5+HEADER_LEN]
|
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]
|
// header: [StreamID 4 bytes][Seq 4 bytes][Closing 1 byte][extraLen 1 bytes][random 2 bytes]
|
||||||
putU32(header[0:4], f.StreamID)
|
putU32(header[0:4], f.StreamID)
|
||||||
|
|
@ -54,16 +72,16 @@ func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
|
||||||
prand.Read(header[10:12])
|
prand.Read(header[10:12])
|
||||||
|
|
||||||
if payloadCipher == nil {
|
if payloadCipher == nil {
|
||||||
copy(encryptedPayload, f.Payload)
|
copy(encryptedPayloadWithExtra, f.Payload)
|
||||||
if extraLen != 0 {
|
if extraLen != 0 {
|
||||||
rand.Read(encryptedPayload[len(encryptedPayload)-int(extraLen):])
|
rand.Read(encryptedPayloadWithExtra[len(encryptedPayloadWithExtra)-int(extraLen):])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ciphertext := payloadCipher.Seal(nil, header, f.Payload, nil)
|
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)
|
salsa20.XORKeyStream(header, header, nonce, &salsaKey)
|
||||||
|
|
||||||
// Composing final obfsed message
|
// Composing final obfsed message
|
||||||
|
|
@ -71,7 +89,7 @@ func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
|
||||||
recordLayer[0] = 0x17
|
recordLayer[0] = 0x17
|
||||||
recordLayer[1] = 0x03
|
recordLayer[1] = 0x03
|
||||||
recordLayer[2] = 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 usefulLen, nil
|
||||||
}
|
}
|
||||||
return obfs
|
return obfs
|
||||||
|
|
@ -87,7 +105,7 @@ func MakeDeobfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Deobfser {
|
||||||
copy(peeled, in[5:])
|
copy(peeled, in[5:])
|
||||||
|
|
||||||
header := peeled[:12]
|
header := peeled[:12]
|
||||||
pldWithOverHead := peeled[12:] // plaintext + potential overhead
|
pldWithOverHead := peeled[12:] // payload + potential overhead
|
||||||
|
|
||||||
nonce := peeled[len(peeled)-8:]
|
nonce := peeled[len(peeled)-8:]
|
||||||
salsa20.XORKeyStream(header, header, nonce, &salsaKey)
|
salsa20.XORKeyStream(header, header, nonce, &salsaKey)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const (
|
||||||
var ErrBrokenSession = errors.New("broken session")
|
var ErrBrokenSession = errors.New("broken session")
|
||||||
var errRepeatSessionClosing = errors.New("trying to close a closed session")
|
var errRepeatSessionClosing = errors.New("trying to close a closed session")
|
||||||
|
|
||||||
|
// Obfuscator is responsible for the obfuscation and deobfuscation of frames
|
||||||
type Obfuscator struct {
|
type Obfuscator struct {
|
||||||
// Used in Stream.Write. Add multiplexing headers, encrypt and add TLS header
|
// Used in Stream.Write. Add multiplexing headers, encrypt and add TLS header
|
||||||
Obfs Obfser
|
Obfs Obfser
|
||||||
|
|
@ -33,7 +34,7 @@ type SessionConfig struct {
|
||||||
|
|
||||||
Valve
|
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)
|
UnitRead func(net.Conn, []byte) (int, error)
|
||||||
|
|
||||||
Unordered bool
|
Unordered bool
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import (
|
||||||
|
|
||||||
var ErrBrokenStream = errors.New("broken stream")
|
var ErrBrokenStream = errors.New("broken stream")
|
||||||
|
|
||||||
|
// ReadWriteCloseLener is io.ReadWriteCloser with Len()
|
||||||
|
// used for bufferedPipe and datagramBuffer
|
||||||
type ReadWriteCloseLener interface {
|
type ReadWriteCloseLener interface {
|
||||||
io.ReadWriteCloser
|
io.ReadWriteCloser
|
||||||
Len() int
|
Len() int
|
||||||
|
|
@ -34,6 +36,7 @@ type Stream struct {
|
||||||
|
|
||||||
writingM sync.RWMutex
|
writingM sync.RWMutex
|
||||||
|
|
||||||
|
// atomic
|
||||||
closed uint32
|
closed uint32
|
||||||
|
|
||||||
obfsBuf []byte
|
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
|
// 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
|
// 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
|
// overall the streams in a session should be uniformly distributed across all connections
|
||||||
|
// This is not used in unordered connection mode
|
||||||
assignedConnId uint32
|
assignedConnId uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,8 +68,6 @@ func makeStream(sesh *Session, id uint32, assignedConnId uint32) *Stream {
|
||||||
return 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) isClosed() bool { return atomic.LoadUint32(&s.closed) == 1 }
|
||||||
|
|
||||||
func (s *Stream) writeFrame(frame *Frame) {
|
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) {
|
func (s *Stream) Read(buf []byte) (n int, err error) {
|
||||||
//log.Tracef("attempting to read from stream %v", s.id)
|
//log.Tracef("attempting to read from stream %v", s.id)
|
||||||
if len(buf) == 0 {
|
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) {
|
func (s *Stream) Write(in []byte) (n int, err error) {
|
||||||
// RWMutex used here isn't really for RW.
|
// RWMutex used here isn't really for RW.
|
||||||
// we use it to exploit the fact that RLock doesn't create contention.
|
// 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) {
|
func (sb *switchboard) assignRandomConn() (uint32, error) {
|
||||||
sb.connsM.RLock()
|
sb.connsM.RLock()
|
||||||
defer sb.connsM.RUnlock()
|
defer sb.connsM.RUnlock()
|
||||||
|
|
@ -124,6 +125,7 @@ func (sb *switchboard) assignRandomConn() (uint32, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// actively triggered by session.Close()
|
// actively triggered by session.Close()
|
||||||
|
// TODO: closeALl needs to clear the conns map
|
||||||
func (sb *switchboard) closeAll() {
|
func (sb *switchboard) closeAll() {
|
||||||
if atomic.SwapUint32(&sb.broken, 1) == 1 {
|
if atomic.SwapUint32(&sb.broken, 1) == 1 {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -199,9 +199,8 @@ func composeServerHello(sessionId []byte, sharedSecret []byte, sessionKey []byte
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// composeReply composes the ServerHello, ChangeCipherSpec and Finished messages
|
// composeReply composes the ServerHello, ChangeCipherSpec and an ApplicationData messages
|
||||||
// together with their respective record layers into one byte slice. The content
|
// together with their respective record layers into one byte slice.
|
||||||
// of these messages are random and useless for this plugin
|
|
||||||
func composeReply(ch *ClientHello, sharedSecret []byte, sessionKey []byte) ([]byte, error) {
|
func composeReply(ch *ClientHello, sharedSecret []byte, sessionKey []byte) ([]byte, error) {
|
||||||
TLS12 := []byte{0x03, 0x03}
|
TLS12 := []byte{0x03, 0x03}
|
||||||
sh, err := composeServerHello(ch.sessionId, sharedSecret, sessionKey)
|
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 ErrReplay = errors.New("duplicate random")
|
||||||
var ErrBadProxyMethod = errors.New("invalid proxy method")
|
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) {
|
func PrepareConnection(firstPacket []byte, sta *State, conn net.Conn) (info ClientInfo, finisher func([]byte) error, err error) {
|
||||||
ch, err := parseClientHello(firstPacket)
|
ch, err := parseClientHello(firstPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ type ActiveUser struct {
|
||||||
sessions map[uint32]*mux.Session
|
sessions map[uint32]*mux.Session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteSession closes a session and removes its reference from the user
|
||||||
func (u *ActiveUser) DeleteSession(sessionID uint32, reason string) {
|
func (u *ActiveUser) DeleteSession(sessionID uint32, reason string) {
|
||||||
u.sessionsM.Lock()
|
u.sessionsM.Lock()
|
||||||
sesh, existing := u.sessions[sessionID]
|
sesh, existing := u.sessions[sessionID]
|
||||||
|
|
@ -34,6 +35,9 @@ func (u *ActiveUser) DeleteSession(sessionID uint32, reason string) {
|
||||||
u.sessionsM.Unlock()
|
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) {
|
func (u *ActiveUser) GetSession(sessionID uint32, config *mux.SessionConfig) (sesh *mux.Session, existing bool, err error) {
|
||||||
u.sessionsM.Lock()
|
u.sessionsM.Lock()
|
||||||
defer u.sessionsM.Unlock()
|
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) {
|
func (u *ActiveUser) Terminate(reason string) {
|
||||||
u.sessionsM.Lock()
|
u.sessionsM.Lock()
|
||||||
for _, sesh := range u.sessions {
|
for _, sesh := range u.sessions {
|
||||||
|
|
@ -66,6 +71,7 @@ func (u *ActiveUser) Terminate(reason string) {
|
||||||
u.panel.DeleteActiveUser(u)
|
u.panel.DeleteActiveUser(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NumSession returns the number of active sessions
|
||||||
func (u *ActiveUser) NumSession() int {
|
func (u *ActiveUser) NumSession() int {
|
||||||
u.sessionsM.RLock()
|
u.sessionsM.RLock()
|
||||||
defer u.sessionsM.RUnlock()
|
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 ErrCiphertextLength = errors.New("ciphertext has the wrong length")
|
||||||
var ErrTimestampOutOfWindow = errors.New("timestamp is outside of the accepting window")
|
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) {
|
func touchStone(ch *ClientHello, staticPv crypto.PrivateKey, now func() time.Time) (info ClientInfo, sharedSecret []byte, err error) {
|
||||||
ephPub, ok := ecdh.Unmarshal(ch.random)
|
ephPub, ok := ecdh.Unmarshal(ch.random)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ func InitState(bindHost, bindPort string, nowFunc func() time.Time) (*State, err
|
||||||
return ret, nil
|
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) {
|
func (sta *State) ParseConfig(conf string) (err error) {
|
||||||
var content []byte
|
var content []byte
|
||||||
var preParse rawConfig
|
var preParse rawConfig
|
||||||
|
|
@ -83,7 +83,6 @@ func (sta *State) ParseConfig(conf string) (err error) {
|
||||||
|
|
||||||
if preParse.CncMode {
|
if preParse.CncMode {
|
||||||
//TODO: implement command & control mode
|
//TODO: implement command & control mode
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
manager, err := usermanager.MakeLocalManager(preParse.DatabasePath)
|
manager, err := usermanager.MakeLocalManager(preParse.DatabasePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -143,6 +142,7 @@ func (sta *State) ParseConfig(conf string) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsBypass checks if a UID is a bypass user
|
||||||
func (sta *State) IsBypass(UID []byte) bool {
|
func (sta *State) IsBypass(UID []byte) bool {
|
||||||
var arrUID [16]byte
|
var arrUID [16]byte
|
||||||
copy(arrUID[:], UID)
|
copy(arrUID[:], UID)
|
||||||
|
|
@ -154,7 +154,7 @@ const TIMESTAMP_TOLERANCE = 180 * time.Second
|
||||||
|
|
||||||
const CACHE_CLEAN_INTERVAL = 12 * time.Hour
|
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() {
|
func (sta *State) UsedRandomCleaner() {
|
||||||
for {
|
for {
|
||||||
time.Sleep(CACHE_CLEAN_INTERVAL)
|
time.Sleep(CACHE_CLEAN_INTERVAL)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ func i32ToB(value int32) []byte {
|
||||||
return nib
|
return nib
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// localManager is responsible for routing API calls to appropriate handlers and manage the local user database accordingly
|
||||||
type localManager struct {
|
type localManager struct {
|
||||||
db *bolt.DB
|
db *bolt.DB
|
||||||
Router *gmux.Router
|
Router *gmux.Router
|
||||||
|
|
@ -63,6 +64,8 @@ func (manager *localManager) registerMux() *gmux.Router {
|
||||||
return r
|
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) {
|
func (manager *localManager) AuthenticateUser(UID []byte) (int64, int64, error) {
|
||||||
var upRate, downRate, upCredit, downCredit, expiryTime int64
|
var upRate, downRate, upCredit, downCredit, expiryTime int64
|
||||||
err := manager.db.View(func(tx *bolt.Tx) error {
|
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
|
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 {
|
func (manager *localManager) AuthoriseNewSession(UID []byte, ainfo AuthorisationInfo) error {
|
||||||
var arrUID [16]byte
|
var arrUID [16]byte
|
||||||
copy(arrUID[:], UID)
|
copy(arrUID[:], UID)
|
||||||
|
|
@ -128,6 +133,9 @@ func (manager *localManager) AuthoriseNewSession(UID []byte, ainfo Authorisation
|
||||||
return nil
|
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) {
|
func (manager *localManager) UploadStatus(uploads []StatusUpdate) ([]StatusResponse, error) {
|
||||||
var responses []StatusResponse
|
var responses []StatusResponse
|
||||||
err := manager.db.Update(func(tx *bolt.Tx) error {
|
err := manager.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ func MakeUserPanel(manager usermanager.UserManager) *userPanel {
|
||||||
return ret
|
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) {
|
func (panel *userPanel) GetBypassUser(UID []byte) (*ActiveUser, error) {
|
||||||
panel.activeUsersM.Lock()
|
panel.activeUsersM.Lock()
|
||||||
var arrUID [16]byte
|
var arrUID [16]byte
|
||||||
|
|
@ -49,6 +50,8 @@ func (panel *userPanel) GetBypassUser(UID []byte) (*ActiveUser, error) {
|
||||||
return user, nil
|
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) {
|
func (panel *userPanel) GetUser(UID []byte) (*ActiveUser, error) {
|
||||||
panel.activeUsersM.Lock()
|
panel.activeUsersM.Lock()
|
||||||
var arrUID [16]byte
|
var arrUID [16]byte
|
||||||
|
|
@ -76,7 +79,9 @@ func (panel *userPanel) GetUser(UID []byte) (*ActiveUser, error) {
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteActiveUser deletes the references to the active user
|
||||||
func (panel *userPanel) DeleteActiveUser(user *ActiveUser) {
|
func (panel *userPanel) DeleteActiveUser(user *ActiveUser) {
|
||||||
|
// TODO: terminate the user here?
|
||||||
panel.updateUsageQueueForOne(user)
|
panel.updateUsageQueueForOne(user)
|
||||||
panel.activeUsersM.Lock()
|
panel.activeUsersM.Lock()
|
||||||
delete(panel.activeUsers, user.arrUID)
|
delete(panel.activeUsers, user.arrUID)
|
||||||
|
|
@ -97,6 +102,7 @@ type usagePair struct {
|
||||||
down *int64
|
down *int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateUsageQueue zeroes the accumulated usage all ActiveUsers valve and put the usage data im usageUpdateQueue
|
||||||
func (panel *userPanel) updateUsageQueue() {
|
func (panel *userPanel) updateUsageQueue() {
|
||||||
panel.activeUsersM.Lock()
|
panel.activeUsersM.Lock()
|
||||||
panel.usageUpdateQueueM.Lock()
|
panel.usageUpdateQueueM.Lock()
|
||||||
|
|
@ -119,6 +125,8 @@ func (panel *userPanel) updateUsageQueue() {
|
||||||
panel.usageUpdateQueueM.Unlock()
|
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) {
|
func (panel *userPanel) updateUsageQueueForOne(user *ActiveUser) {
|
||||||
// used when one particular user deactivates
|
// used when one particular user deactivates
|
||||||
if user.bypass {
|
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 {
|
func (panel *userPanel) commitUpdate() error {
|
||||||
panel.usageUpdateQueueM.Lock()
|
panel.usageUpdateQueueM.Lock()
|
||||||
statuses := make([]usermanager.StatusUpdate, 0, len(panel.usageUpdateQueue))
|
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
|
left := dataLength
|
||||||
readPtr := 5
|
readPtr := 5
|
||||||
|
|
||||||
|
// TODO: Deadline here?
|
||||||
for left != 0 {
|
for left != 0 {
|
||||||
// If left > buffer size (i.e. our message got segmented), the entire MTU is read
|
// 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, the entire buffer is all there left to read
|
||||||
// if left < buffer size (i.e. multiple messages came together),
|
// if left < buffer size (i.e. multiple messages came together),
|
||||||
// only the message we want is read
|
// 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])
|
i, err = io.ReadFull(conn, buffer[readPtr:readPtr+left])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue