Refactor client config

pull/108/head
Andy Wang 4 years ago
parent 402cfc9e25
commit a163f066a6

@ -4,220 +4,16 @@ package main
import (
"encoding/base64"
"encoding/binary"
"flag"
"fmt"
"io"
"net"
"os"
"sync"
"sync/atomic"
"time"
"github.com/cbeuw/Cloak/internal/client"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/Cloak/internal/util"
log "github.com/sirupsen/logrus"
"os"
)
var version string
func makeSession(sta *client.State, isAdmin bool) *mux.Session {
log.Info("Attempting to start a new session")
if !isAdmin {
// sessionID is usergenerated. There shouldn't be a security concern because the scope of
// sessionID is limited to its UID.
quad := make([]byte, 4)
util.CryptoRandRead(quad)
atomic.StoreUint32(&sta.SessionID, binary.BigEndian.Uint32(quad))
}
d := net.Dialer{Control: protector, KeepAlive: sta.KeepAlive}
connsCh := make(chan net.Conn, sta.NumConn)
var _sessionKey atomic.Value
var wg sync.WaitGroup
for i := 0; i < sta.NumConn; i++ {
wg.Add(1)
go func() {
makeconn:
remoteConn, err := d.Dial("tcp", net.JoinHostPort(sta.RemoteHost, sta.RemotePort))
if err != nil {
log.Errorf("Failed to establish new connections to remote: %v", err)
// TODO increase the interval if failed multiple times
time.Sleep(time.Second * 3)
goto makeconn
}
var sk []byte
remoteConn, sk, err = sta.Transport.PrepareConnection(sta, remoteConn)
if err != nil {
remoteConn.Close()
log.Errorf("Failed to prepare connection to remote: %v", err)
time.Sleep(time.Second * 3)
goto makeconn
}
_sessionKey.Store(sk)
connsCh <- remoteConn
wg.Done()
}()
}
wg.Wait()
log.Debug("All underlying connections established")
sessionKey := _sessionKey.Load().([]byte)
obfuscator, err := mux.GenerateObfs(sta.EncryptionMethod, sessionKey, sta.Transport.HasRecordLayer())
if err != nil {
log.Fatal(err)
}
seshConfig := &mux.SessionConfig{
Obfuscator: obfuscator,
Valve: nil,
UnitRead: sta.Transport.UnitReadFunc(),
Unordered: sta.Unordered,
}
sesh := mux.MakeSession(sta.SessionID, seshConfig)
for i := 0; i < sta.NumConn; i++ {
conn := <-connsCh
sesh.AddConnection(conn)
}
log.Infof("Session %v established", sta.SessionID)
return sesh
}
func routeUDP(sta *client.State, adminUID []byte) {
var sesh *mux.Session
localUDPAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(sta.LocalHost, sta.LocalPort))
if err != nil {
log.Fatal(err)
}
start:
localConn, err := net.ListenUDP("udp", localUDPAddr)
if err != nil {
log.Fatal(err)
}
var otherEnd atomic.Value
data := make([]byte, 10240)
i, oe, err := localConn.ReadFromUDP(data)
if err != nil {
log.Errorf("Failed to read first packet from proxy client: %v", err)
localConn.Close()
return
}
otherEnd.Store(oe)
if sesh == nil || sesh.IsClosed() {
sesh = makeSession(sta, adminUID != nil)
}
log.Debugf("proxy local address %v", otherEnd.Load().(*net.UDPAddr).String())
stream, err := sesh.OpenStream()
if err != nil {
log.Errorf("Failed to open stream: %v", err)
localConn.Close()
//localConnWrite.Close()
return
}
_, err = stream.Write(data[:i])
if err != nil {
log.Errorf("Failed to write to stream: %v", err)
localConn.Close()
//localConnWrite.Close()
stream.Close()
return
}
// stream to proxy
go func() {
buf := make([]byte, 16380)
for {
i, err := io.ReadAtLeast(stream, buf, 1)
if err != nil {
log.Print(err)
localConn.Close()
stream.Close()
break
}
_, err = localConn.WriteToUDP(buf[:i], otherEnd.Load().(*net.UDPAddr))
if err != nil {
log.Print(err)
localConn.Close()
stream.Close()
break
}
}
}()
// proxy to stream
buf := make([]byte, 16380)
if sta.Timeout != 0 {
localConn.SetReadDeadline(time.Now().Add(sta.Timeout))
}
for {
if sta.Timeout != 0 {
localConn.SetReadDeadline(time.Now().Add(sta.Timeout))
}
i, oe, err := localConn.ReadFromUDP(buf)
if err != nil {
localConn.Close()
stream.Close()
break
}
otherEnd.Store(oe)
_, err = stream.Write(buf[:i])
if err != nil {
localConn.Close()
stream.Close()
break
}
}
goto start
}
func routeTCP(sta *client.State, adminUID []byte) {
tcpListener, err := net.Listen("tcp", net.JoinHostPort(sta.LocalHost, sta.LocalPort))
if err != nil {
log.Fatal(err)
}
var sesh *mux.Session
for {
localConn, err := tcpListener.Accept()
if err != nil {
log.Fatal(err)
continue
}
if sesh == nil || sesh.IsClosed() {
sesh = makeSession(sta, adminUID != nil)
}
go func() {
data := make([]byte, 10240)
i, err := io.ReadAtLeast(localConn, data, 1)
if err != nil {
log.Errorf("Failed to read first packet from proxy client: %v", err)
localConn.Close()
return
}
stream, err := sesh.OpenStream()
if err != nil {
log.Errorf("Failed to open stream: %v", err)
localConn.Close()
return
}
_, err = stream.Write(data[:i])
if err != nil {
log.Errorf("Failed to write to stream: %v", err)
localConn.Close()
stream.Close()
return
}
go util.Pipe(localConn, stream, 0)
util.Pipe(stream, localConn, sta.Timeout)
}()
}
}
func main() {
// Should be 127.0.0.1 to listen to a proxy client on this machine
var localHost string
@ -234,16 +30,12 @@ func main() {
log_init()
ssPluginMode := os.Getenv("SS_LOCAL_HOST") != ""
verbosity := flag.String("verbosity", "info", "verbosity level")
if os.Getenv("SS_LOCAL_HOST") != "" {
localHost = os.Getenv("SS_LOCAL_HOST")
localPort = os.Getenv("SS_LOCAL_PORT")
remoteHost = os.Getenv("SS_REMOTE_HOST")
remotePort = os.Getenv("SS_REMOTE_PORT")
if ssPluginMode {
config = os.Getenv("SS_PLUGIN_OPTIONS")
flag.Parse() // for verbosity only
} else {
flag.StringVar(&localHost, "i", "127.0.0.1", "localHost: Cloak listens to proxy clients on this ip")
flag.StringVar(&localPort, "l", "1984", "localPort: Cloak listens to proxy clients on this port")
@ -255,6 +47,9 @@ func main() {
flag.StringVar(&b64AdminUID, "a", "", "adminUID: enter the adminUID to serve the admin api")
askVersion := flag.Bool("v", false, "Print the version number")
printUsage := flag.Bool("h", false, "Print this message")
// commandline arguments overrides json
flag.Parse()
if *askVersion {
@ -276,36 +71,62 @@ func main() {
}
log.SetLevel(lvl)
sta := &client.State{
LocalHost: localHost,
LocalPort: localPort,
RemotePort: remotePort,
Now: time.Now,
}
err = sta.ParseConfig(config)
rawConfig, err := client.ParseConfig(config)
if err != nil {
log.Fatal(err)
}
if proxyMethod != "" {
sta.ProxyMethod = proxyMethod
}
if remoteHost != "" {
sta.RemoteHost = remoteHost
}
if os.Getenv("SS_LOCAL_HOST") != "" {
sta.ProxyMethod = "shadowsocks"
if ssPluginMode {
rawConfig.ProxyMethod = "shadowsocks"
// json takes precedence over environment variables
// i.e. if json field isn't empty, use that
if rawConfig.RemoteHost == "" {
rawConfig.RemoteHost = os.Getenv("SS_REMOTE_HOST")
}
if rawConfig.RemotePort == "" {
rawConfig.RemoteHost = os.Getenv("SS_REMOTE_PORT")
}
if rawConfig.LocalHost == "" {
rawConfig.LocalHost = os.Getenv("SS_LOCAL_HOST")
}
if rawConfig.LocalPort == "" {
rawConfig.LocalPort = os.Getenv("SS_LOCAL_PORT")
}
} else {
// commandline argument takes precedence over json
// if commandline argument is set, use commandline
flag.Visit(func(f *flag.Flag) {
// manually set ones
switch f.Name {
case "i":
rawConfig.LocalHost = localHost
case "l":
rawConfig.LocalPort = localPort
case "s":
rawConfig.RemoteHost = remoteHost
case "p":
rawConfig.RemotePort = remotePort
case "proxy":
rawConfig.ProxyMethod = proxyMethod
}
})
// ones with default values
if rawConfig.LocalHost == "" {
rawConfig.LocalHost = localHost
}
if rawConfig.LocalPort == "" {
rawConfig.LocalPort = localPort
}
if rawConfig.RemotePort == "" {
rawConfig.RemotePort = remotePort
}
}
if sta.LocalPort == "" {
log.Fatal("Must specify localPort")
}
if sta.RemoteHost == "" {
log.Fatal("Must specify remoteHost")
localConfig, remoteConfig, authInfo, err := rawConfig.SplitConfigs()
if err != nil {
log.Fatal(err)
}
remoteConfig.Protector = protector
var adminUID []byte
if b64AdminUID != "" {
@ -315,26 +136,34 @@ func main() {
}
}
var seshMaker func() *mux.Session
if adminUID != nil {
log.Infof("API base is %v", net.JoinHostPort(sta.LocalHost, sta.LocalPort))
sta.SessionID = 0
sta.UID = adminUID
sta.NumConn = 1
log.Infof("API base is %v", localConfig.LocalAddr)
authInfo.UID = adminUID
remoteConfig.NumConn = 1
seshMaker = func() *mux.Session {
return client.MakeSession(remoteConfig, authInfo, true)
}
} else {
var network string
if udp {
network = "UDP"
sta.Unordered = true
authInfo.Unordered = true
} else {
network = "TCP"
sta.Unordered = false
authInfo.Unordered = false
}
log.Infof("Listening on %v %v for %v client", network, localConfig.LocalAddr, authInfo.ProxyMethod)
seshMaker = func() *mux.Session {
return client.MakeSession(remoteConfig, authInfo, false)
}
log.Infof("Listening on %v %v for %v client", network, net.JoinHostPort(sta.LocalHost, sta.LocalPort), sta.ProxyMethod)
}
if udp {
routeUDP(sta, adminUID)
client.RouteUDP(localConfig, seshMaker)
} else {
routeTCP(sta, adminUID)
client.RouteTCP(localConfig, seshMaker)
}
}

@ -5,6 +5,7 @@ import (
"encoding/binary"
"github.com/cbeuw/Cloak/internal/util"
"net"
"time"
log "github.com/sirupsen/logrus"
)
@ -45,7 +46,7 @@ func addExtRec(typ []byte, data []byte) []byte {
return ret
}
func unmarshalAuthenticationInfo(ai authenticationPayload, serverName string) (ret clientHelloFields) {
func genStegClientHello(ai authenticationPayload, serverName string) (ret clientHelloFields) {
// random is marshalled ephemeral pub key 32 bytes
// The authentication ciphertext and its tag are then distributed among SessionId and X25519KeyShare
ret.random = ai.randPubKey[:]
@ -56,7 +57,7 @@ func unmarshalAuthenticationInfo(ai authenticationPayload, serverName string) (r
}
type DirectTLS struct {
Transport
browser
}
func (DirectTLS) HasRecordLayer() bool { return true }
@ -64,10 +65,10 @@ func (DirectTLS) UnitReadFunc() func(net.Conn, []byte) (int, error) { return uti
// PrepareConnection handles the TLS handshake for a given conn and returns the sessionKey
// if the server proceed with Cloak authentication
func (DirectTLS) PrepareConnection(sta *State, conn net.Conn) (preparedConn net.Conn, sessionKey []byte, err error) {
func (tls DirectTLS) PrepareConnection(authInfo *authInfo, conn net.Conn) (preparedConn net.Conn, sessionKey []byte, err error) {
preparedConn = conn
payload, sharedSecret := makeAuthenticationPayload(sta, rand.Reader)
chOnly := sta.browser.composeClientHello(unmarshalAuthenticationInfo(payload, sta.ServerName))
payload, sharedSecret := makeAuthenticationPayload(authInfo, rand.Reader, time.Now())
chOnly := tls.browser.composeClientHello(genStegClientHello(payload, authInfo.MockDomain))
chWithRecordLayer := util.AddRecordLayer(chOnly, []byte{0x16}, []byte{0x03, 0x01})
_, err = preparedConn.Write(chWithRecordLayer)
if err != nil {

@ -1,11 +1,12 @@
package client
import (
"crypto"
"encoding/binary"
"github.com/cbeuw/Cloak/internal/ecdh"
"github.com/cbeuw/Cloak/internal/util"
"io"
"sync/atomic"
"time"
)
const (
@ -17,9 +18,19 @@ type authenticationPayload struct {
ciphertextWithTag [64]byte
}
type authInfo struct {
UID []byte
SessionId uint32
ProxyMethod string
EncryptionMethod byte
Unordered bool
ServerPubKey crypto.PublicKey
MockDomain string
}
// makeAuthenticationPayload generates the ephemeral key pair, calculates the shared secret, and then compose and
// encrypt the authenticationPayload
func makeAuthenticationPayload(sta *State, randReader io.Reader) (ret authenticationPayload, sharedSecret []byte) {
func makeAuthenticationPayload(authInfo *authInfo, randReader io.Reader, time time.Time) (ret authenticationPayload, sharedSecret []byte) {
/*
Authentication data:
+----------+----------------+---------------------+-------------+--------------+--------+------------+
@ -32,17 +43,17 @@ func makeAuthenticationPayload(sta *State, randReader io.Reader) (ret authentica
copy(ret.randPubKey[:], ecdh.Marshal(ephPub))
plaintext := make([]byte, 48)
copy(plaintext, sta.UID)
copy(plaintext[16:28], sta.ProxyMethod)
plaintext[28] = sta.EncryptionMethod
binary.BigEndian.PutUint64(plaintext[29:37], uint64(sta.Now().Unix()))
binary.BigEndian.PutUint32(plaintext[37:41], atomic.LoadUint32(&sta.SessionID))
copy(plaintext, authInfo.UID)
copy(plaintext[16:28], authInfo.ProxyMethod)
plaintext[28] = authInfo.EncryptionMethod
binary.BigEndian.PutUint64(plaintext[29:37], uint64(time.Unix()))
binary.BigEndian.PutUint32(plaintext[37:41], authInfo.SessionId)
if sta.Unordered {
if authInfo.Unordered {
plaintext[41] |= UNORDERED_FLAG
}
sharedSecret = ecdh.GenerateSharedSecret(ephPv, sta.staticPub)
sharedSecret = ecdh.GenerateSharedSecret(ephPv, authInfo.ServerPubKey)
ciphertextWithTag, _ := util.AESGCMEncrypt(ret.randPubKey[:12], sharedSecret, plaintext)
copy(ret.ciphertextWithTag[:], ciphertextWithTag[:])
return

@ -0,0 +1,88 @@
package client
import (
"encoding/binary"
"net"
"sync"
"sync/atomic"
"syscall"
"time"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/Cloak/internal/util"
log "github.com/sirupsen/logrus"
)
type remoteConnConfig struct {
NumConn int
KeepAlive time.Duration
Protector func(string, string, syscall.RawConn) error
RemoteAddr string
Transport Transport
}
func MakeSession(connConfig *remoteConnConfig, authInfo *authInfo, isAdmin bool) *mux.Session {
log.Info("Attempting to start a new session")
if !isAdmin {
// sessionID is usergenerated. There shouldn't be a security concern because the scope of
// sessionID is limited to its UID.
quad := make([]byte, 4)
util.CryptoRandRead(quad)
authInfo.SessionId = binary.BigEndian.Uint32(quad)
} else {
authInfo.SessionId = 0
}
d := net.Dialer{Control: connConfig.Protector, KeepAlive: connConfig.KeepAlive}
connsCh := make(chan net.Conn, connConfig.NumConn)
var _sessionKey atomic.Value
var wg sync.WaitGroup
for i := 0; i < connConfig.NumConn; i++ {
wg.Add(1)
go func() {
makeconn:
remoteConn, err := d.Dial("tcp", connConfig.RemoteAddr)
if err != nil {
log.Errorf("Failed to establish new connections to remote: %v", err)
// TODO increase the interval if failed multiple times
time.Sleep(time.Second * 3)
goto makeconn
}
var sk []byte
remoteConn, sk, err = connConfig.Transport.PrepareConnection(authInfo, remoteConn)
if err != nil {
remoteConn.Close()
log.Errorf("Failed to prepare connection to remote: %v", err)
time.Sleep(time.Second * 3)
goto makeconn
}
_sessionKey.Store(sk)
connsCh <- remoteConn
wg.Done()
}()
}
wg.Wait()
log.Debug("All underlying connections established")
sessionKey := _sessionKey.Load().([]byte)
obfuscator, err := mux.GenerateObfs(authInfo.EncryptionMethod, sessionKey, connConfig.Transport.HasRecordLayer())
if err != nil {
log.Fatal(err)
}
seshConfig := &mux.SessionConfig{
Obfuscator: obfuscator,
Valve: nil,
UnitRead: connConfig.Transport.UnitReadFunc(),
Unordered: authInfo.Unordered,
}
sesh := mux.MakeSession(authInfo.SessionId, seshConfig)
for i := 0; i < connConfig.NumConn; i++ {
conn := <-connsCh
sesh.AddConnection(conn)
}
log.Infof("Session %v established", authInfo.SessionId)
return sesh
}

@ -0,0 +1,144 @@
package client
import (
"io"
"net"
"sync/atomic"
"time"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/Cloak/internal/util"
log "github.com/sirupsen/logrus"
)
func RouteUDP(localConfig *localConnConfig, newSeshFunc func() *mux.Session) {
var sesh *mux.Session
localUDPAddr, err := net.ResolveUDPAddr("udp", localConfig.LocalAddr)
if err != nil {
log.Fatal(err)
}
start:
localConn, err := net.ListenUDP("udp", localUDPAddr)
if err != nil {
log.Fatal(err)
}
var otherEnd atomic.Value
data := make([]byte, 10240)
i, oe, err := localConn.ReadFromUDP(data)
if err != nil {
log.Errorf("Failed to read first packet from proxy client: %v", err)
localConn.Close()
return
}
otherEnd.Store(oe)
if sesh == nil || sesh.IsClosed() {
sesh = newSeshFunc()
}
log.Debugf("proxy local address %v", otherEnd.Load().(*net.UDPAddr).String())
stream, err := sesh.OpenStream()
if err != nil {
log.Errorf("Failed to open stream: %v", err)
localConn.Close()
//localConnWrite.Close()
return
}
_, err = stream.Write(data[:i])
if err != nil {
log.Errorf("Failed to write to stream: %v", err)
localConn.Close()
//localConnWrite.Close()
stream.Close()
return
}
// stream to proxy
go func() {
buf := make([]byte, 16380)
for {
i, err := io.ReadAtLeast(stream, buf, 1)
if err != nil {
log.Print(err)
localConn.Close()
stream.Close()
break
}
_, err = localConn.WriteToUDP(buf[:i], otherEnd.Load().(*net.UDPAddr))
if err != nil {
log.Print(err)
localConn.Close()
stream.Close()
break
}
}
}()
// proxy to stream
buf := make([]byte, 16380)
if localConfig.Timeout != 0 {
localConn.SetReadDeadline(time.Now().Add(localConfig.Timeout))
}
for {
if localConfig.Timeout != 0 {
localConn.SetReadDeadline(time.Now().Add(localConfig.Timeout))
}
i, oe, err := localConn.ReadFromUDP(buf)
if err != nil {
localConn.Close()
stream.Close()
break
}
otherEnd.Store(oe)
_, err = stream.Write(buf[:i])
if err != nil {
localConn.Close()
stream.Close()
break
}
}
goto start
}
func RouteTCP(localConfig *localConnConfig, newSeshFunc func() *mux.Session) {
tcpListener, err := net.Listen("tcp", localConfig.LocalAddr)
if err != nil {
log.Fatal(err)
}
var sesh *mux.Session
for {
localConn, err := tcpListener.Accept()
if err != nil {
log.Fatal(err)
continue
}
if sesh == nil || sesh.IsClosed() {
sesh = newSeshFunc()
}
go func() {
data := make([]byte, 10240)
i, err := io.ReadAtLeast(localConn, data, 1)
if err != nil {
log.Errorf("Failed to read first packet from proxy client: %v", err)
localConn.Close()
return
}
stream, err := sesh.OpenStream()
if err != nil {
log.Errorf("Failed to open stream: %v", err)
localConn.Close()
return
}
_, err = stream.Write(data[:i])
if err != nil {
log.Errorf("Failed to write to stream: %v", err)
localConn.Close()
stream.Close()
return
}
go util.Pipe(localConn, stream, 0)
util.Pipe(stream, localConn, localConfig.Timeout)
}()
}
}

@ -1,11 +1,10 @@
package client
import (
"crypto"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"strconv"
"net"
"strings"
"time"
@ -14,54 +13,51 @@ import (
)
// rawConfig represents the fields in the config json file
// nullable means if it's empty, a default value will be chosen in SplitConfigs
// jsonOptional means if the json's empty, its value will be set from environment variables or commandline args
// but it mustn't be empty when SplitConfigs is called
type rawConfig struct {
ServerName string
ProxyMethod string
EncryptionMethod string
UID []byte
PublicKey []byte
BrowserSig string
Transport string
NumConn int
StreamTimeout int
KeepAlive int
RemoteHost string
RemotePort int
LocalHost string // jsonOptional
LocalPort string // jsonOptional
RemoteHost string // jsonOptional
RemotePort string // jsonOptional
//TODO: udp
// defaults set in SplitConfigs
BrowserSig string // nullable
Transport string // nullable
StreamTimeout int // nullable
KeepAlive int // nullable
}
// State stores the parsed configuration fields
type State struct {
LocalHost string
LocalPort string
RemoteHost string
RemotePort string
Unordered bool
Transport Transport
SessionID uint32
UID []byte
staticPub crypto.PublicKey
Now func() time.Time // for easier testing
browser browser
ProxyMethod string
EncryptionMethod byte
ServerName string
NumConn int
Timeout time.Duration
KeepAlive time.Duration
type localConnConfig struct {
LocalAddr string
Timeout time.Duration
}
// semi-colon separated value. This is for Android plugin options
func ssvToJson(ssv string) (ret []byte) {
elem := func(val string, lst []string) bool {
for _, v := range lst {
if val == v {
return true
}
}
return false
}
unescape := func(s string) string {
r := strings.Replace(s, `\\`, `\`, -1)
r = strings.Replace(r, `\=`, `=`, -1)
r = strings.Replace(r, `\;`, `;`, -1)
return r
}
unquoted := []string{"NumConn", "StreamTimeout", "KeepAlive"}
lines := strings.Split(unescape(ssv), ";")
ret = []byte("{")
for _, ln := range lines {
@ -73,7 +69,7 @@ func ssvToJson(ssv string) (ret []byte) {
value := sp[1]
// 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" {
if elem(key, unquoted) {
ret = append(ret, []byte(`"`+key+`":`+value+`,`)...)
} else {
ret = append(ret, []byte(`"`+key+`":"`+value+`",`)...)
@ -84,8 +80,7 @@ func ssvToJson(ssv string) (ret []byte) {
return ret
}
// 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 ParseConfig(conf string) (raw *rawConfig, err error) {
var content []byte
// Checking if it's a path to json or a ssv string
if strings.Contains(conf, ";") && strings.Contains(conf, "=") {
@ -93,75 +88,116 @@ func (sta *State) ParseConfig(conf string) (err error) {
} else {
content, err = ioutil.ReadFile(conf)
if err != nil {
return err
return
}
}
var preParse rawConfig
err = json.Unmarshal(content, &preParse)
raw = new(rawConfig)
err = json.Unmarshal(content, &raw)
if err != nil {
return err
return
}
return
}
switch strings.ToLower(preParse.EncryptionMethod) {
func (raw *rawConfig) SplitConfigs() (local *localConnConfig, remote *remoteConnConfig, auth *authInfo, err error) {
nullErr := func(field string) (local *localConnConfig, remote *remoteConnConfig, auth *authInfo, err error) {
err = fmt.Errorf("%v cannot be empty", field)
return
}
auth = new(authInfo)
if raw.ServerName == "" {
return nullErr("ServerName")
}
auth.MockDomain = raw.ServerName
if raw.ProxyMethod == "" {
return nullErr("ServerName")
}
auth.ProxyMethod = raw.ProxyMethod
if len(raw.UID) == 0 {
return nullErr("UID")
}
auth.UID = raw.UID
// static public key
if len(raw.PublicKey) == 0 {
return nullErr("PublicKey")
}
pub, ok := ecdh.Unmarshal(raw.PublicKey)
if !ok {
err = fmt.Errorf("failed to unmarshal Public key")
return
}
auth.ServerPubKey = pub
// Encryption method
switch strings.ToLower(raw.EncryptionMethod) {
case "plain":
sta.EncryptionMethod = mux.E_METHOD_PLAIN
auth.EncryptionMethod = mux.E_METHOD_PLAIN
case "aes-gcm":
sta.EncryptionMethod = mux.E_METHOD_AES_GCM
auth.EncryptionMethod = mux.E_METHOD_AES_GCM
case "chacha20-poly1305":
sta.EncryptionMethod = mux.E_METHOD_CHACHA20_POLY1305
auth.EncryptionMethod = mux.E_METHOD_CHACHA20_POLY1305
default:
return errors.New("Unknown encryption method")
err = fmt.Errorf("unknown encryption method %v", raw.EncryptionMethod)
return
}
switch strings.ToLower(preParse.BrowserSig) {
case "chrome":
sta.browser = &Chrome{}
case "firefox":
sta.browser = &Firefox{}
default:
return errors.New("unsupported browser signature")
remote = new(remoteConnConfig)
if raw.RemoteHost == "" {
return nullErr("RemoteHost")
}
if raw.RemotePort == "" {
return nullErr("RemotePort")
}
remote.RemoteAddr = net.JoinHostPort(raw.RemoteHost, raw.RemotePort)
if raw.NumConn == 0 {
return nullErr("NumConn")
}
remote.NumConn = raw.NumConn
switch strings.ToLower(preParse.Transport) {
case "direct":
sta.Transport = DirectTLS{}
// Transport and (if TLS mode), browser
switch strings.ToLower(raw.Transport) {
case "cdn":
sta.Transport = WSOverTLS{}
remote.Transport = WSOverTLS{remote.RemoteAddr}
case "direct":
fallthrough
default:
sta.Transport = DirectTLS{}
var browser browser
switch strings.ToLower(raw.BrowserSig) {
case "firefox":
browser = &Firefox{}
case "chrome":
fallthrough
default:
browser = &Chrome{}
}
remote.Transport = DirectTLS{browser}
}
sta.RemoteHost = preParse.RemoteHost
sta.ProxyMethod = preParse.ProxyMethod
sta.ServerName = preParse.ServerName
sta.NumConn = preParse.NumConn
if preParse.StreamTimeout == 0 {
sta.Timeout = 300 * time.Second
// KeepAlive
if raw.KeepAlive <= 0 {
remote.KeepAlive = -1
} else {
sta.Timeout = time.Duration(preParse.StreamTimeout) * time.Second
remote.KeepAlive = remote.KeepAlive * time.Second
}
if preParse.KeepAlive <= 0 {
sta.KeepAlive = -1
} else {
sta.KeepAlive = time.Duration(preParse.KeepAlive) * time.Second
}
sta.UID = preParse.UID
pub, ok := ecdh.Unmarshal(preParse.PublicKey)
if !ok {
return errors.New("Failed to unmarshal Public key")
}
sta.staticPub = pub
local = new(localConnConfig)
// OPTIONAL: set RemotePort via JSON
// if RemotePort is specified in the JSON we overwrite sta.RemotePort
// if not, don't do anything, since sta.RemotePort is already initialised in ck-client.go
if preParse.RemotePort != 0 {
// basic validity check
if preParse.RemotePort >= 1 && preParse.RemotePort <= 65535 {
sta.RemotePort = strconv.Itoa(preParse.RemotePort)
}
if raw.LocalHost == "" {
return nullErr("LocalHost")
}
if raw.LocalPort == "" {
return nullErr("LocalPort")
}
local.LocalAddr = net.JoinHostPort(raw.LocalHost, raw.LocalPort)
// stream no write timeout
if raw.StreamTimeout == 0 {
local.Timeout = 300 * time.Second
} else {
local.Timeout = time.Duration(raw.StreamTimeout) * time.Second
}
return nil
return
}

@ -3,7 +3,7 @@ package client
import "net"
type Transport interface {
PrepareConnection(*State, net.Conn) (net.Conn, []byte, error)
PrepareConnection(*authInfo, net.Conn) (net.Conn, []byte, error)
HasRecordLayer() bool
UnitReadFunc() func(net.Conn, []byte) (int, error)
}

@ -10,35 +10,36 @@ import (
"net"
"net/http"
"net/url"
"time"
utls "github.com/refraction-networking/utls"
)
type WSOverTLS struct {
Transport
cdnDomainPort string
}
func (WSOverTLS) HasRecordLayer() bool { return false }
func (WSOverTLS) UnitReadFunc() func(net.Conn, []byte) (int, error) { return util.ReadWebSocket }
func (WSOverTLS) PrepareConnection(sta *State, conn net.Conn) (preparedConn net.Conn, sessionKey []byte, err error) {
func (ws WSOverTLS) PrepareConnection(authInfo *authInfo, cdnConn net.Conn) (preparedConn net.Conn, sessionKey []byte, err error) {
utlsConfig := &utls.Config{
ServerName: sta.ServerName,
ServerName: authInfo.MockDomain,
InsecureSkipVerify: true,
}
uconn := utls.UClient(conn, utlsConfig, utls.HelloChrome_Auto)
uconn := utls.UClient(cdnConn, utlsConfig, utls.HelloChrome_Auto)
err = uconn.Handshake()
preparedConn = uconn
if err != nil {
return
}
u, err := url.Parse("ws://" + sta.RemoteHost + ":" + sta.RemotePort) //TODO IPv6
u, err := url.Parse("ws://" + ws.cdnDomainPort)
if err != nil {
return preparedConn, nil, fmt.Errorf("failed to parse ws url: %v", err)
}
payload, sharedSecret := makeAuthenticationPayload(sta, rand.Reader)
payload, sharedSecret := makeAuthenticationPayload(authInfo, rand.Reader, time.Now())
header := http.Header{}
header.Add("hidden", base64.StdEncoding.EncodeToString(append(payload.randPubKey[:], payload.ciphertextWithTag[:]...)))
c, _, err := websocket.NewClient(preparedConn, u, header, 16480, 16480)

Loading…
Cancel
Save