mirror of https://github.com/cbeuw/Cloak
321 lines
9.1 KiB
Go
321 lines
9.1 KiB
Go
package client
|
|
|
|
import (
|
|
"crypto"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cbeuw/Cloak/internal/common"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/cbeuw/Cloak/internal/ecdh"
|
|
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
|
)
|
|
|
|
// RawConfig represents the fields in the config json file
|
|
// 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 ProcessRawConfig is called
|
|
type RawConfig struct {
|
|
// Required fields
|
|
// ServerName is the domain you appear to be visiting
|
|
// to your Firewall or ISP
|
|
ServerName string
|
|
// ProxyMethod is the name of the underlying proxy you wish
|
|
// to connect to, as determined by your server. The value can
|
|
// be any string whose UTF-8 ENCODED byte length is no greater than
|
|
// 12 bytes
|
|
ProxyMethod string
|
|
// UID is a 16-byte secret string unique to an authorised user
|
|
// The same UID can be used by the same user for multiple Cloak connections
|
|
UID []byte
|
|
// PublicKey is the 32-byte public Curve25519 ECDH key of your server
|
|
PublicKey []byte
|
|
// RemoteHost is the Cloak server's hostname or IP address
|
|
RemoteHost string // jsonOptional
|
|
|
|
// Optional Fields
|
|
// EncryptionMethod is the cryptographic algorithm used to
|
|
// encrypt data on the wire.
|
|
// Valid values are `aes-128-gcm`, `aes-256-gcm`, `chacha20-poly1305`, and `plain`
|
|
// Defaults to `aes-256-gcm`
|
|
EncryptionMethod string
|
|
// NumConn is the amount of underlying TLS connections to establish with Cloak server.
|
|
// Cloak multiplexes any number of incoming connections to a fixed number of underlying TLS connections.
|
|
// If set to 0, a special singleplex mode is enabled: each incoming connection will correspond to exactly one
|
|
// TLS connection
|
|
// Defaults to 4
|
|
NumConn *int
|
|
// UDP enables UDP semantics, where packets must fit into one unit of message (below 16000 bytes by default),
|
|
// and packets can be received out of order. Though reliable delivery is still guaranteed.
|
|
UDP bool
|
|
// BrowserSig is the browser signature to be used. Options are `chrome` and `firefox`
|
|
// Defaults to `chrome`
|
|
BrowserSig string
|
|
// Transport is either `direct` or `cdn`. Under `direct`, the client connects to a Cloak server directly.
|
|
// Under `cdn`, the client connects to a CDN provider such as Amazon Cloudfront, which in turn connects
|
|
// to a Cloak server.
|
|
// Defaults to `direct`
|
|
Transport string
|
|
// CDNOriginHost is the CDN Origin's (i.e. Cloak server) real hostname or IP address, which is encrypted between
|
|
// the client and the CDN server, and therefore hidden to ISP or firewalls. This only has effect when Transport
|
|
// is set to `cdn`
|
|
// Defaults to RemoteHost
|
|
CDNOriginHost string
|
|
// StreamTimeout is the duration, in seconds, for a stream to be automatically closed after the last write.
|
|
// Defaults to 300
|
|
StreamTimeout int
|
|
// KeepAlive is the interval between TCP KeepAlive packets to be sent over the underlying TLS connections
|
|
// Defaults to -1, which means no TCP KeepAlive is ever sent
|
|
KeepAlive int
|
|
// RemotePort is the port Cloak server is listening to
|
|
// Defaults to 443
|
|
RemotePort string
|
|
|
|
// LocalHost is the hostname or IP address to listen for incoming proxy client connections
|
|
LocalHost string // jsonOptional
|
|
// LocalPort is the port to listen for incomig proxy client connections
|
|
LocalPort string // jsonOptional
|
|
// AlternativeNames is a list of ServerName Cloak may randomly pick from for different sessions
|
|
AlternativeNames []string
|
|
}
|
|
|
|
type RemoteConnConfig struct {
|
|
Singleplex bool
|
|
NumConn int
|
|
KeepAlive time.Duration
|
|
RemoteAddr string
|
|
TransportMaker func() transports.Transport
|
|
}
|
|
|
|
type LocalConnConfig struct {
|
|
LocalAddr string
|
|
Timeout time.Duration
|
|
MockDomainList []string
|
|
}
|
|
|
|
type AuthInfo = transports.AuthInfo
|
|
|
|
// 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", "UDP"}
|
|
lines := strings.Split(unescape(ssv), ";")
|
|
ret = []byte("{")
|
|
for _, ln := range lines {
|
|
if ln == "" {
|
|
break
|
|
}
|
|
sp := strings.SplitN(ln, "=", 2)
|
|
if len(sp) < 2 {
|
|
log.Errorf("Malformed config option: %v", ln)
|
|
continue
|
|
}
|
|
key := sp[0]
|
|
value := sp[1]
|
|
if strings.HasPrefix(key, "AlternativeNames") {
|
|
switch strings.Contains(value, ",") {
|
|
case true:
|
|
domains := strings.Split(value, ",")
|
|
for index, domain := range domains {
|
|
domains[index] = `"` + domain + `"`
|
|
}
|
|
value = strings.Join(domains, ",")
|
|
ret = append(ret, []byte(`"`+key+`":[`+value+`],`)...)
|
|
case false:
|
|
ret = append(ret, []byte(`"`+key+`":["`+value+`"],`)...)
|
|
}
|
|
continue
|
|
}
|
|
// JSON doesn't like quotation marks around int and bool
|
|
// This is extremely ugly but it's still better than writing a tokeniser
|
|
if elem(key, unquoted) {
|
|
ret = append(ret, []byte(`"`+key+`":`+value+`,`)...)
|
|
} else {
|
|
ret = append(ret, []byte(`"`+key+`":"`+value+`",`)...)
|
|
}
|
|
}
|
|
ret = ret[:len(ret)-1] // remove the last comma
|
|
ret = append(ret, '}')
|
|
return ret
|
|
}
|
|
|
|
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, "=") {
|
|
content = ssvToJson(conf)
|
|
} else {
|
|
content, err = ioutil.ReadFile(conf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
raw = new(RawConfig)
|
|
err = json.Unmarshal(content, &raw)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (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.UID = raw.UID
|
|
auth.Unordered = raw.UDP
|
|
if raw.ServerName == "" {
|
|
return nullErr("ServerName")
|
|
}
|
|
auth.MockDomain = raw.ServerName
|
|
|
|
var filteredAlternativeNames []string
|
|
for _, alternativeName := range raw.AlternativeNames {
|
|
if len(alternativeName) > 0 {
|
|
filteredAlternativeNames = append(filteredAlternativeNames, alternativeName)
|
|
}
|
|
}
|
|
raw.AlternativeNames = filteredAlternativeNames
|
|
|
|
local.MockDomainList = raw.AlternativeNames
|
|
local.MockDomainList = append(local.MockDomainList, auth.MockDomain)
|
|
if raw.ProxyMethod == "" {
|
|
return nullErr("ServerName")
|
|
}
|
|
auth.ProxyMethod = raw.ProxyMethod
|
|
if len(raw.UID) == 0 {
|
|
return nullErr("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
|
|
auth.WorldState = worldState
|
|
|
|
// Encryption method
|
|
switch strings.ToLower(raw.EncryptionMethod) {
|
|
case "plain":
|
|
auth.EncryptionMethod = mux.EncryptionMethodPlain
|
|
case "aes-gcm", "aes-256-gcm":
|
|
auth.EncryptionMethod = mux.EncryptionMethodAES256GCM
|
|
case "aes-128-gcm":
|
|
auth.EncryptionMethod = mux.EncryptionMethodAES128GCM
|
|
case "chacha20-poly1305":
|
|
auth.EncryptionMethod = mux.EncryptionMethodChaha20Poly1305
|
|
case "":
|
|
auth.EncryptionMethod = mux.EncryptionMethodAES256GCM
|
|
default:
|
|
err = fmt.Errorf("unknown encryption method %v", raw.EncryptionMethod)
|
|
return
|
|
}
|
|
|
|
if raw.RemoteHost == "" {
|
|
return nullErr("RemoteHost")
|
|
}
|
|
var remotePort string
|
|
if raw.RemotePort == "" {
|
|
remotePort = "443"
|
|
} else {
|
|
remotePort = raw.RemotePort
|
|
}
|
|
remote.RemoteAddr = net.JoinHostPort(raw.RemoteHost, remotePort)
|
|
if raw.NumConn == nil {
|
|
remote.NumConn = 4
|
|
remote.Singleplex = false
|
|
} else if *raw.NumConn <= 0 {
|
|
remote.NumConn = 1
|
|
remote.Singleplex = true
|
|
} else {
|
|
remote.NumConn = *raw.NumConn
|
|
remote.Singleplex = false
|
|
}
|
|
|
|
// Transport and (if TLS mode), browser
|
|
switch strings.ToLower(raw.Transport) {
|
|
case "cdn":
|
|
cdnPort := raw.RemotePort
|
|
var cdnHost string
|
|
if raw.CDNOriginHost == "" {
|
|
cdnHost = raw.RemoteHost
|
|
} else {
|
|
cdnHost = raw.CDNOriginHost
|
|
}
|
|
|
|
remote.TransportMaker = func() transports.Transport {
|
|
return &transports.WSOverTLS{
|
|
CDNHost: cdnHost,
|
|
CDNPort: cdnPort,
|
|
}
|
|
}
|
|
case "direct":
|
|
fallthrough
|
|
default:
|
|
var browser browser
|
|
switch strings.ToLower(raw.BrowserSig) {
|
|
case "firefox":
|
|
browser = firefox
|
|
case "safari":
|
|
browser = safari
|
|
case "chrome":
|
|
fallthrough
|
|
default:
|
|
browser = chrome
|
|
}
|
|
remote.TransportMaker = func() transports.Transport {
|
|
return &transports.DirectTLS{
|
|
Browser: browser,
|
|
}
|
|
}
|
|
}
|
|
|
|
// KeepAlive
|
|
if raw.KeepAlive <= 0 {
|
|
remote.KeepAlive = -1
|
|
} else {
|
|
remote.KeepAlive = remote.KeepAlive * time.Second
|
|
}
|
|
|
|
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
|
|
}
|