Untested server

pull/2/head
Qian Wang 6 years ago
parent 3fd7e01566
commit ae30ed6ba4

@ -20,16 +20,9 @@ import (
var version string
func pipe(dst io.ReadWriteCloser, src io.ReadWriteCloser) {
buf := make([]byte, 20480)
for {
i, err := src.Read(buf)
if err != nil {
go dst.Close()
go src.Close()
return
}
_, err = dst.Write(buf[:i])
if err != nil {
i, err := io.Copy(dst, src)
if err != nil || i == 0 {
go dst.Close()
go src.Close()
return

@ -0,0 +1,209 @@
package main
import (
"flag"
"fmt"
"io"
"log"
"net"
"os"
"strings"
"time"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/Cloak/internal/server"
"github.com/cbeuw/Cloak/internal/util"
)
var version string
func pipe(dst io.ReadWriteCloser, src io.ReadWriteCloser) {
for {
i, err := io.Copy(dst, src)
if err != nil || i == 0 {
go dst.Close()
go src.Close()
return
}
}
}
func dispatchConnection(conn net.Conn, sta *server.State) {
goWeb := func(data []byte) {
webConn, err := net.Dial("tcp", sta.WebServerAddr)
if err != nil {
log.Printf("Making connection to redirection server: %v\n", err)
go webConn.Close()
return
}
webConn.Write(data)
go pipe(webConn, conn)
go pipe(conn, webConn)
}
buf := make([]byte, 1500)
conn.SetReadDeadline(time.Now().Add(3 * time.Second))
i, err := io.ReadAtLeast(conn, buf, 1)
if err != nil {
go conn.Close()
return
}
conn.SetReadDeadline(time.Time{})
data := buf[:i]
ch, err := server.ParseClientHello(data)
if err != nil {
log.Printf("+1 non SS non (or malformed) TLS traffic from %v\n", conn.RemoteAddr())
goWeb(data)
return
}
isSS, SID := server.TouchStone(ch, sta)
if !isSS {
log.Printf("+1 non SS TLS traffic from %v\n", conn.RemoteAddr())
goWeb(data)
return
}
// TODO: verify SID
reply := server.ComposeReply(ch)
_, err = conn.Write(reply)
if err != nil {
log.Printf("Sending reply to remote: %v\n", err)
go conn.Close()
return
}
// Two discarded messages: ChangeCipherSpec and Finished
discardBuf := make([]byte, 1024)
for c := 0; c < 2; c++ {
_, err = util.ReadTillDrain(conn, discardBuf)
if err != nil {
log.Printf("Reading discarded message %v: %v\n", c, err)
go conn.Close()
return
}
}
go func() {
var arrSID [32]byte
copy(arrSID[:], SID)
sesh := sta.GetSession(arrSID)
if sesh == nil {
sesh.AddConnection(conn)
} else {
sesh := mux.MakeSession(0, conn, util.MakeObfs(SID), util.MakeDeobfs(SID), util.ReadTillDrain)
sta.PutSession(arrSID, sesh)
}
go func() {
for {
newStream, err := sesh.AcceptStream()
if err != nil {
log.Printf("Failed to get new stream: %v", err)
}
ssConn, err := net.Dial("tcp", sta.SS_LOCAL_HOST+":"+sta.SS_LOCAL_PORT)
if err != nil {
log.Printf("Failed to connect to ssserver: %v", err)
}
go pipe(ssConn, newStream)
go pipe(newStream, ssConn)
}
}()
}()
}
func main() {
// Should be 127.0.0.1 to listen to ss-server on this machine
var localHost string
// server_port in ss config, same as remotePort in plugin mode
var localPort string
// server in ss config, the outbound listening ip
var remoteHost string
// Outbound listening ip, should be 443
var remotePort string
var pluginOpts string
log.SetFlags(log.LstdFlags | log.Lshortfile)
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")
pluginOpts = os.Getenv("SS_PLUGIN_OPTIONS")
} else {
localAddr := flag.String("r", "", "localAddr: 127.0.0.1:server_port as set in SS config")
flag.StringVar(&remoteHost, "s", "0.0.0.0", "remoteHost: outbound listing ip, set to 0.0.0.0 to listen to everything")
flag.StringVar(&remotePort, "p", "443", "remotePort: outbound listing port, should be 443")
flag.StringVar(&pluginOpts, "c", "server.json", "pluginOpts: path to server.json or options seperated by semicolons")
askVersion := flag.Bool("v", false, "Print the version number")
printUsage := flag.Bool("h", false, "Print this message")
flag.Parse()
if *askVersion {
fmt.Printf("ck-server %s\n", version)
return
}
if *printUsage {
flag.Usage()
return
}
if *localAddr == "" {
log.Fatal("Must specify localAddr")
}
localHost = strings.Split(*localAddr, ":")[0]
localPort = strings.Split(*localAddr, ":")[1]
log.Printf("Starting standalone mode, listening on %v:%v to ss at %v:%v\n", remoteHost, remotePort, localHost, localPort)
}
sta := &server.State{
SS_LOCAL_HOST: localHost,
SS_LOCAL_PORT: localPort,
SS_REMOTE_HOST: remoteHost,
SS_REMOTE_PORT: remotePort,
Now: time.Now,
UsedRandom: map[[32]byte]int{},
}
err := sta.ParseConfig(pluginOpts)
if err != nil {
log.Fatalf("Configuration file error: %v", err)
}
go sta.UsedRandomCleaner()
listen := func(addr, port string) {
listener, err := net.Listen("tcp", addr+":"+port)
log.Println("Listening on " + addr + ":" + port)
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("%v", err)
continue
}
go dispatchConnection(conn, sta)
}
}
// When listening on an IPv6 and IPv4, SS gives REMOTE_HOST as e.g. ::|0.0.0.0
listeningIP := strings.Split(sta.SS_REMOTE_HOST, "|")
for i, ip := range listeningIP {
if net.ParseIP(ip).To4() == nil {
// IPv6 needs square brackets
ip = "[" + ip + "]"
}
// The last listener must block main() because the program exits on main return.
if i == len(listeningIP)-1 {
listen(ip, sta.SS_REMOTE_PORT)
} else {
go listen(ip, sta.SS_REMOTE_PORT)
}
}
}

@ -0,0 +1,6 @@
{
"ServerName":"www.bing.com",
"Key":"UNhY4JhezH9gQYqvDMWrWH9CwlcKiECVqejMrND2VFwEOF8c8XRX8iYVdjKW2BAfym2zppExMPteovDB/Q8phdD53FnH39tQ1daaVLn9+FIGOAdk+UZZ2aOt5jSK638YPg==",
"TicketTimeHint":3600,
"Browser":"chrome"
}

@ -0,0 +1,4 @@
{
"WebServerAddr":"204.79.197.200:443",
"Key":"H2pMM834RzkouOoRGNhbiQRnm4Ggy8sg+S6ve5yYfqUEOF8c8XRX8iYVdjKW2BAfym2zppExMPteovDB/Q8phdD53FnH39tQ1daaVLn9+FIGOAdk+UZZ2aOt5jSK638YPg=="
}

@ -47,7 +47,7 @@ func MakeSessionTicket(sta *State) []byte {
// Then the hmac is 32 bytes
//
// 65+56+32=153
ct, _ := ecies.Encrypt(rand.Reader, sta.Pub, plain, nil, nil)
ct, _ := ecies.Encrypt(rand.Reader, sta.pub, plain, nil, nil)
sessionTicket := make([]byte, 192)
// The reason for ct[1:] is that, the first byte of ct is always 0x04
// This is specified in the section 4.3.6 of ANSI X9.62 (the uncompressed form).

@ -27,7 +27,7 @@ type State struct {
SS_REMOTE_PORT string
Now func() time.Time
SID []byte
Pub *ecies.PublicKey
pub *ecies.PublicKey
TicketTimeHint int
ServerName string
MaskBrowser string
@ -88,10 +88,11 @@ func (sta *State) ParseConfig(conf string) (err error) {
return errors.New("Failed to parse Key: " + err.Error())
}
sta.SID = sid
sta.Pub = pub
sta.pub = pub
return nil
}
// Structure: [SID 32 bytes][marshalled public key]
func parseKey(b64 string) ([]byte, *ecies.PublicKey, error) {
b, err := base64.StdEncoding.DecodeString(b64)
if err != nil {

@ -0,0 +1,163 @@
package server
import (
"encoding/binary"
"errors"
"time"
"github.com/cbeuw/Cloak/internal/util"
)
// ClientHello contains every field in a ClientHello message
type ClientHello struct {
handshakeType byte
length int
clientVersion []byte
random []byte
sessionIdLen int
sessionId []byte
cipherSuitesLen int
cipherSuites []byte
compressionMethodsLen int
compressionMethods []byte
extensionsLen int
extensions map[[2]byte][]byte
}
func parseExtensions(input []byte) (ret map[[2]byte][]byte, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("Malformed Extensions")
}
}()
pointer := 0
totalLen := len(input)
ret = make(map[[2]byte][]byte)
for pointer < totalLen {
var typ [2]byte
copy(typ[:], input[pointer:pointer+2])
pointer += 2
length := util.BtoInt(input[pointer : pointer+2])
pointer += 2
data := input[pointer : pointer+length]
pointer += length
ret[typ] = data
}
return ret, err
}
// AddRecordLayer adds record layer to data
func AddRecordLayer(input []byte, typ []byte, ver []byte) []byte {
length := make([]byte, 2)
binary.BigEndian.PutUint16(length, uint16(len(input)))
ret := make([]byte, 5+len(input))
copy(ret[0:1], typ)
copy(ret[1:3], ver)
copy(ret[3:5], length)
copy(ret[5:], input)
return ret
}
// PeelRecordLayer peels off the record layer
func PeelRecordLayer(data []byte) []byte {
ret := data[5:]
return ret
}
// ParseClientHello parses everything on top of the TLS layer
// (including the record layer) into ClientHello type
func ParseClientHello(data []byte) (ret *ClientHello, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("Malformed ClientHello")
}
}()
data = PeelRecordLayer(data)
pointer := 0
// Handshake Type
handshakeType := data[pointer]
if handshakeType != 0x01 {
return ret, errors.New("Not a ClientHello")
}
pointer += 1
// Length
length := util.BtoInt(data[pointer : pointer+3])
pointer += 3
if length != len(data[pointer:]) {
return ret, errors.New("Hello length doesn't match")
}
// Client Version
clientVersion := data[pointer : pointer+2]
pointer += 2
// Random
random := data[pointer : pointer+32]
pointer += 32
// Session ID
sessionIdLen := int(data[pointer])
pointer += 1
sessionId := data[pointer : pointer+sessionIdLen]
pointer += sessionIdLen
// Cipher Suites
cipherSuitesLen := util.BtoInt(data[pointer : pointer+2])
pointer += 2
cipherSuites := data[pointer : pointer+cipherSuitesLen]
pointer += cipherSuitesLen
// Compression Methods
compressionMethodsLen := int(data[pointer])
pointer += 1
compressionMethods := data[pointer : pointer+compressionMethodsLen]
pointer += compressionMethodsLen
// Extensions
extensionsLen := util.BtoInt(data[pointer : pointer+2])
pointer += 2
extensions, err := parseExtensions(data[pointer:])
ret = &ClientHello{
handshakeType,
length,
clientVersion,
random,
sessionIdLen,
sessionId,
cipherSuitesLen,
cipherSuites,
compressionMethodsLen,
compressionMethods,
extensionsLen,
extensions,
}
return
}
func composeServerHello(ch *ClientHello) []byte {
var serverHello [10][]byte
serverHello[0] = []byte{0x02} // handshake type
serverHello[1] = []byte{0x00, 0x00, 0x4d} // length 77
serverHello[2] = []byte{0x03, 0x03} // server version
serverHello[3] = util.PsudoRandBytes(32, time.Now().UnixNano()) // random
serverHello[4] = []byte{0x20} // session id length 32
serverHello[5] = ch.sessionId // session id
serverHello[6] = []byte{0xc0, 0x30} // cipher suite TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
serverHello[7] = []byte{0x00} // compression method null
serverHello[8] = []byte{0x00, 0x05} // extensions length 5
serverHello[9] = []byte{0xff, 0x01, 0x00, 0x01, 0x00} // extensions renegotiation_info
ret := []byte{}
for i := 0; i < 10; i++ {
ret = append(ret, serverHello[i]...)
}
return ret
}
// 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
func ComposeReply(ch *ClientHello) []byte {
TLS12 := []byte{0x03, 0x03}
shBytes := AddRecordLayer(composeServerHello(ch), []byte{0x16}, TLS12)
ccsBytes := AddRecordLayer([]byte{0x01}, []byte{0x14}, TLS12)
finished := make([]byte, 64)
finished = util.PsudoRandBytes(40, time.Now().UnixNano())
fBytes := AddRecordLayer(finished, []byte{0x16}, TLS12)
ret := append(shBytes, ccsBytes...)
ret = append(ret, fBytes...)
return ret
}

@ -0,0 +1,55 @@
package server
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"github.com/cbeuw/ecies"
"log"
)
// input ticket, return SID
func decryptSessionTicket(pv *ecies.PrivateKey, ticket []byte) ([]byte, error) {
ciphertext := make([]byte, 153)
ciphertext[0] = 0x04
copy(ciphertext[1:], ticket)
plaintext, err := pv.Decrypt(ciphertext, nil, nil)
if err != nil {
return nil, err
}
return plaintext[0:32], nil
}
func validateRandom(random []byte, SID []byte, time int64) bool {
t := make([]byte, 8)
binary.BigEndian.PutUint64(t, uint64(time/12*60*60))
rand := random[0:16]
preHash := make([]byte, 56)
copy(preHash[0:32], SID)
copy(preHash[32:40], t)
copy(preHash[40:56], rand)
h := sha256.New()
h.Write(preHash)
return bytes.Equal(h.Sum(nil)[0:16], random[16:32])
}
func TouchStone(ch *ClientHello, sta *State) (bool, []byte) {
var random [32]byte
copy(random[:], ch.random)
used := sta.getUsedRandom(random)
if used != 0 {
log.Println("Replay! Duplicate random")
return false, nil
}
sta.putUsedRandom(random)
SID, err := decryptSessionTicket(sta.pv, ch.extensions[[2]byte{0x00, 0x23}])
if err != nil {
return false, nil
}
isSS := validateRandom(ch.random, SID, sta.Now().Unix())
if !isSS {
return false, nil
}
return true, SID
}

@ -0,0 +1,159 @@
package server
import (
"crypto/elliptic"
"encoding/base64"
"encoding/json"
"io/ioutil"
"math/big"
"strings"
"sync"
"time"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/ecies"
)
type rawConfig struct {
WebServerAddr string
Key string
}
type stateManager interface {
ParseConfig(string) error
SetAESKey(string)
PutUsedRandom([32]byte)
}
// State type stores the global state of the program
type State struct {
WebServerAddr string
Now func() time.Time
SS_LOCAL_HOST string
SS_LOCAL_PORT string
SS_REMOTE_HOST string
SS_REMOTE_PORT string
UsedRandomM sync.RWMutex
UsedRandom map[[32]byte]int
pv *ecies.PrivateKey
SessionsM sync.RWMutex
Sessions map[[32]byte]*mux.Session
}
// semi-colon separated value.
func ssvToJson(ssv string) (ret []byte) {
unescape := func(s string) string {
r := strings.Replace(s, "\\\\", "\\", -1)
r = strings.Replace(r, "\\=", "=", -1)
r = strings.Replace(r, "\\;", ";", -1)
return r
}
lines := strings.Split(unescape(ssv), ";")
ret = []byte("{")
for _, ln := range lines {
if ln == "" {
break
}
sp := strings.SplitN(ln, "=", 2)
key := sp[0]
value := sp[1]
ret = append(ret, []byte("\""+key+"\":\""+value+"\",")...)
}
ret = ret[:len(ret)-1] // remove the last comma
ret = append(ret, '}')
return ret
}
// Structue: [D 32 bytes][marshalled public key]
func parseKey(b64 string) (*ecies.PrivateKey, error) {
b, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return nil, err
}
d := b[0:32]
marshalled := b[32:]
x, y := elliptic.Unmarshal(ecies.DefaultCurve, marshalled)
pub := ecies.PublicKey{
X: x,
Y: y,
Curve: ecies.DefaultCurve,
Params: ecies.ParamsFromCurve(ecies.DefaultCurve),
}
pv := &ecies.PrivateKey{
PublicKey: pub,
D: new(big.Int).SetBytes(d),
}
return pv, nil
}
// ParseConfig parses the config (either a path to json or in-line ssv config) into a State variable
func (sta *State) ParseConfig(conf string) (err error) {
var content []byte
if strings.Contains(conf, ";") && strings.Contains(conf, "=") {
content = ssvToJson(conf)
} else {
content, err = ioutil.ReadFile(conf)
if err != nil {
return err
}
}
var preParse rawConfig
err = json.Unmarshal(content, &preParse)
if err != nil {
return err
}
sta.WebServerAddr = preParse.WebServerAddr
pv, err := parseKey(preParse.Key)
sta.pv = pv
return nil
}
func (sta *State) GetSession(SID [32]byte) *mux.Session {
sta.SessionsM.Lock()
defer sta.SessionsM.Unlock()
if sesh, ok := sta.Sessions[SID]; ok {
return sesh
} else {
return nil
}
}
func (sta *State) PutSession(SID [32]byte, sesh *mux.Session) {
sta.SessionsM.Lock()
sta.Sessions[SID] = sesh
sta.SessionsM.Unlock()
}
func (sta *State) getUsedRandom(random [32]byte) int {
sta.UsedRandomM.Lock()
defer sta.UsedRandomM.Unlock()
return sta.UsedRandom[random]
}
// PutUsedRandom adds a random field into map UsedRandom
func (sta *State) putUsedRandom(random [32]byte) {
sta.UsedRandomM.Lock()
sta.UsedRandom[random] = int(sta.Now().Unix())
sta.UsedRandomM.Unlock()
}
// UsedRandomCleaner clears the cache of used random fields every 12 hours
func (sta *State) UsedRandomCleaner() {
for {
time.Sleep(12 * time.Hour)
now := int(sta.Now().Unix())
sta.UsedRandomM.Lock()
for key, t := range sta.UsedRandom {
if now-t > 12*3600 {
delete(sta.UsedRandom, key)
}
}
sta.UsedRandomM.Unlock()
}
}

@ -33,16 +33,17 @@ func MakeObfs(key []byte) func(*mux.Frame) []byte {
binary.BigEndian.PutUint32(header[4:8], f.Seq)
binary.BigEndian.PutUint32(header[8:12], f.ClosingStreamID)
// header: [StreamID 4 bytes][Seq 4 bytes][ClosingStreamID 4 bytes]
plaintext := make([]byte, 12+len(f.Payload)-16)
copy(plaintext[0:12], header)
copy(plaintext[12:], f.Payload[16:])
// plaintext: [header 12 bytes][Payload[16:]]
plainheader := make([]byte, 16)
copy(plainheader[0:12], header)
copy(plainheader[12:], []byte{0x00, 0x00, 0x00, 0x00})
// plainheader: [header 12 bytes][0x00,0x00,0x00,0x00]
iv := f.Payload[0:16]
ciphertext := encrypt(iv, key, plaintext)
obfsed := make([]byte, 16+len(ciphertext))
cipherheader := encrypt(iv, key, plainheader)
obfsed := make([]byte, len(f.Payload)+12+4)
copy(obfsed[0:16], iv)
copy(obfsed[16:], ciphertext)
// obfsed: [iv 16 bytes][ciphertext]
copy(obfsed[16:32], cipherheader)
copy(obfsed[32:], f.Payload[16:])
// obfsed: [iv 16 bytes][cipherheader 16 bytes][payload w/o iv]
ret := AddRecordLayer(obfsed, []byte{0x17}, []byte{0x03, 0x03})
return ret
}
@ -52,14 +53,14 @@ func MakeObfs(key []byte) func(*mux.Frame) []byte {
func MakeDeobfs(key []byte) func([]byte) *mux.Frame {
deobfs := func(in []byte) *mux.Frame {
peeled := PeelRecordLayer(in)
plaintext := decrypt(peeled[0:16], key, peeled[16:])
// plaintext: [header 12 bytes][Payload[16:]]
streamID := binary.BigEndian.Uint32(plaintext[0:4])
seq := binary.BigEndian.Uint32(plaintext[4:8])
closingStreamID := binary.BigEndian.Uint32(plaintext[8:12])
payload := make([]byte, len(plaintext)-12)
plainheader := decrypt(peeled[0:16], key, peeled[16:32])
// plainheader: [header 12 bytes][0x00,0x00,0x00,0x00]
streamID := binary.BigEndian.Uint32(plainheader[0:4])
seq := binary.BigEndian.Uint32(plainheader[4:8])
closingStreamID := binary.BigEndian.Uint32(plainheader[8:12])
payload := make([]byte, len(peeled)-12-4)
copy(payload[0:16], peeled[0:16])
copy(payload[16:], plaintext[12:])
copy(payload[16:], peeled[32:])
ret := &mux.Frame{
StreamID: streamID,
Seq: seq,

Loading…
Cancel
Save