Better and more explicit handling of first packet, reduces exposure of active probing

pull/125/head
Andy Wang 4 years ago
parent 2bb102a5b6
commit 253ea94d2a

@ -9,8 +9,10 @@ require (
github.com/juju/ratelimit v1.0.1
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mitchellh/gox v1.0.1 // indirect
github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57
github.com/sirupsen/logrus v1.5.0
github.com/stretchr/testify v1.2.2
go.etcd.io/bbolt v1.3.4
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect

@ -6,6 +6,8 @@ github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
@ -17,6 +19,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI=
github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4=
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57 h1:SL1K0QAuC1b54KoY1pjPWe6kSlsFHwK9/oC960fKrTY=

@ -74,6 +74,7 @@ func (tls *TLSConn) Read(buffer []byte) (n int, err error) {
err = io.ErrShortBuffer
return
}
// we overwrite the record layer here
return io.ReadFull(tls.Conn, buffer[:dataLength])
}

@ -66,18 +66,7 @@ var ErrBadProxyMethod = errors.New("invalid proxy method")
// 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 AuthFirstPacket(firstPacket []byte, sta *State) (info ClientInfo, finisher Responder, err error) {
var transport Transport
switch firstPacket[0] {
case 0x47:
transport = &WebSocket{}
case 0x16:
transport = &TLS{}
default:
err = ErrUnrecognisedProtocol
return
}
func AuthFirstPacket(firstPacket []byte, transport Transport, sta *State) (info ClientInfo, finisher Responder, err error) {
fragments, finisher, err := transport.processFirstPacket(firstPacket, sta.StaticPv)
if err != nil {
return

@ -138,7 +138,7 @@ func TestAuthFirstPacket(t *testing.T) {
t.Run("TLS correct", func(t *testing.T) {
sta := getNewState()
chBytes, _ := hex.DecodeString("1603010200010001fc0303ac530b5778469dbbc3f9a83c6ac35b63aa6a70c2014026ade30f2faf0266f0242068424f320bcad49b4315a761f9f6dec32b0a403c2d8c0ab337608a694c6e411c0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00204655c2c83aaed1db2e89ed17d671fcdc76dc96e36bde8840022f1bda2f31019600170041543af1f8d28b37d984073f40e8361613da502f16e4039f00656f427de0f66480b2e77e3e552e126bb0cc097168f6e5454c7f9501126a2377fb40151f6cfc007e0e002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
info, _, err := AuthFirstPacket(chBytes, sta)
info, _, err := AuthFirstPacket(chBytes, TLS{}, sta)
if err != nil {
t.Errorf("failed to get client info: %v", err)
return
@ -155,12 +155,12 @@ func TestAuthFirstPacket(t *testing.T) {
t.Run("TLS correct but replay", func(t *testing.T) {
sta := getNewState()
chBytes, _ := hex.DecodeString("1603010200010001fc0303ac530b5778469dbbc3f9a83c6ac35b63aa6a70c2014026ade30f2faf0266f0242068424f320bcad49b4315a761f9f6dec32b0a403c2d8c0ab337608a694c6e411c0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00204655c2c83aaed1db2e89ed17d671fcdc76dc96e36bde8840022f1bda2f31019600170041543af1f8d28b37d984073f40e8361613da502f16e4039f00656f427de0f66480b2e77e3e552e126bb0cc097168f6e5454c7f9501126a2377fb40151f6cfc007e0e002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
_, _, err := AuthFirstPacket(chBytes, sta)
_, _, err := AuthFirstPacket(chBytes, TLS{}, sta)
if err != nil {
t.Error("failed to prepare for the first time")
return
}
_, _, err = AuthFirstPacket(chBytes, sta)
_, _, err = AuthFirstPacket(chBytes, TLS{}, sta)
if err != ErrReplay {
t.Errorf("failed to return ErrReplay, got %v instead", err)
return
@ -181,7 +181,7 @@ Sec-WebSocket-Version: 13
Upgrade: websocket
`
info, _, err := AuthFirstPacket([]byte(req), sta)
info, _, err := AuthFirstPacket([]byte(req), WebSocket{}, sta)
if err != nil {
t.Errorf("failed to get client info: %v", err)
return

@ -3,6 +3,8 @@ package server
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/server/usermanager"
"io"
@ -37,21 +39,91 @@ func Serve(l net.Listener, sta *State) {
}
}
func dispatchConnection(conn net.Conn, sta *State) {
remoteAddr := conn.RemoteAddr()
var err error
buf := make([]byte, 1500)
func connReadLine(conn net.Conn, buf []byte) (int, error) {
i := 0
for ; i < len(buf); i++ {
_, err := io.ReadFull(conn, buf[i:i+1])
if err != nil {
return i, err
}
if buf[i] == '\n' {
return i + 1, nil
}
}
return i, io.ErrShortBuffer
}
func readFirstPacket(conn net.Conn, buf []byte, timeout time.Duration) (int, Transport, bool, error) {
conn.SetReadDeadline(time.Now().Add(timeout))
defer conn.SetReadDeadline(time.Time{})
// TODO: potential fingerprint for active probers here
conn.SetReadDeadline(time.Now().Add(3 * time.Second))
i, err := io.ReadAtLeast(conn, buf, 1)
_, err := io.ReadFull(conn, buf[:1])
if err != nil {
log.WithField("remoteAddr", remoteAddr).
Infof("failed to read anything after connection is established: %v", err)
err = fmt.Errorf("read error after connection is established: %v", err)
conn.Close()
return
return 0, nil, false, err
}
conn.SetReadDeadline(time.Time{})
// TODO: give the option to match the protocol with port
bufOffset := 1
var transport Transport
switch buf[0] {
case 0x16:
transport = TLS{}
recordLayerLength := 5
i, err := io.ReadFull(conn, buf[bufOffset:recordLayerLength])
bufOffset += i
if err != nil {
err = fmt.Errorf("read error after connection is established: %v", err)
conn.Close()
return bufOffset, transport, false, err
}
dataLength := int(binary.BigEndian.Uint16(buf[3:5]))
if dataLength+recordLayerLength > len(buf) {
return bufOffset, transport, true, io.ErrShortBuffer
}
i, err = io.ReadFull(conn, buf[recordLayerLength:dataLength+recordLayerLength])
bufOffset += i
if err != nil {
err = fmt.Errorf("read error after connection is established: %v", err)
conn.Close()
return bufOffset, transport, false, err
}
case 0x47:
transport = WebSocket{}
for {
i, err := connReadLine(conn, buf[bufOffset:])
line := buf[bufOffset : bufOffset+i]
bufOffset += i
if err != nil {
if err == io.ErrShortBuffer {
return bufOffset, transport, true, err
} else {
err = fmt.Errorf("error reading first packet: %v", err)
conn.Close()
return bufOffset, transport, false, err
}
}
if bytes.Equal(line, []byte("\r\n")) {
break
}
}
default:
err = fmt.Errorf("unrecognised protocol signature")
return bufOffset, transport, true, ErrUnrecognisedProtocol
}
return bufOffset, transport, true, nil
}
func dispatchConnection(conn net.Conn, sta *State) {
var err error
buf := make([]byte, 1500)
i, transport, redirOnErr, err := readFirstPacket(conn, buf, 15*time.Second)
data := buf[:i]
goWeb := func() {
@ -73,10 +145,21 @@ func dispatchConnection(conn net.Conn, sta *State) {
go io.Copy(conn, webConn)
}
ci, finishHandshake, err := AuthFirstPacket(data, sta)
if err != nil {
log.WithField("remoteAddr", conn.RemoteAddr()).
Warnf("error reading first packet: %v", err)
if redirOnErr {
goWeb()
} else {
conn.Close()
}
return
}
ci, finishHandshake, err := AuthFirstPacket(data, transport, sta)
if err != nil {
log.WithFields(log.Fields{
"remoteAddr": remoteAddr,
"remoteAddr": conn.RemoteAddr(),
"UID": b64(ci.UID),
"sessionId": ci.SessionId,
"proxyMethod": ci.ProxyMethod,
@ -132,7 +215,7 @@ func dispatchConnection(conn net.Conn, sta *State) {
if err != nil {
log.WithFields(log.Fields{
"UID": b64(ci.UID),
"remoteAddr": remoteAddr,
"remoteAddr": conn.RemoteAddr(),
"error": err,
}).Warn("+1 unauthorised UID")
goWeb()

@ -0,0 +1,192 @@
package server
import (
"encoding/hex"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
"io"
"net"
"testing"
"time"
)
type rfpReturnValue struct {
n int
transport Transport
redirOnErr bool
err error
}
const timeout = 500 * time.Millisecond
func TestReadFirstPacket(t *testing.T) {
rfp := func(conn net.Conn, buf []byte, retChan chan<- rfpReturnValue) {
ret := rfpReturnValue{}
ret.n, ret.transport, ret.redirOnErr, ret.err = readFirstPacket(conn, buf, timeout)
retChan <- ret
}
t.Run("Good TLS", func(t *testing.T) {
local, remote := connutil.AsyncPipe()
buf := make([]byte, 1500)
retChan := make(chan rfpReturnValue)
go rfp(remote, buf, retChan)
first, _ := hex.DecodeString("1603010200010001fc0303ac530b5778469dbbc3f9a83c6ac35b63aa6a70c2014026ade30f2faf0266f0242068424f320bcad49b4315a761f9f6dec32b0a403c2d8c0ab337608a694c6e411c0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00204655c2c83aaed1db2e89ed17d671fcdc76dc96e36bde8840022f1bda2f31019600170041543af1f8d28b37d984073f40e8361613da502f16e4039f00656f427de0f66480b2e77e3e552e126bb0cc097168f6e5454c7f9501126a2377fb40151f6cfc007e0e002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
local.Write(first)
ret := <-retChan
assert.Equal(t, len(first), ret.n)
assert.Equal(t, first, buf[:ret.n])
assert.IsType(t, TLS{}, ret.transport)
assert.NoError(t, ret.err)
})
t.Run("Good TLS but buf too small", func(t *testing.T) {
local, remote := connutil.AsyncPipe()
buf := make([]byte, 10)
retChan := make(chan rfpReturnValue)
go rfp(remote, buf, retChan)
first, _ := hex.DecodeString("1603010200010001fc0303ac530b5778469dbbc3f9a83c6ac35b63aa6a70c2014026ade30f2faf0266f0242068424f320bcad49b4315a761f9f6dec32b0a403c2d8c0ab337608a694c6e411c0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00204655c2c83aaed1db2e89ed17d671fcdc76dc96e36bde8840022f1bda2f31019600170041543af1f8d28b37d984073f40e8361613da502f16e4039f00656f427de0f66480b2e77e3e552e126bb0cc097168f6e5454c7f9501126a2377fb40151f6cfc007e0e002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
local.Write(first)
ret := <-retChan
assert.Equal(t, io.ErrShortBuffer, ret.err)
assert.True(t, ret.redirOnErr)
assert.Equal(t, first[:ret.n], buf[:ret.n])
})
t.Run("Incomplete timeout", func(t *testing.T) {
local, remote := connutil.AsyncPipe()
buf := make([]byte, 1500)
retChan := make(chan rfpReturnValue)
go rfp(remote, buf, retChan)
first, _ := hex.DecodeString("160301")
local.Write(first)
select {
case ret := <-retChan:
assert.Equal(t, len(first), ret.n)
assert.False(t, ret.redirOnErr)
assert.Error(t, ret.err)
case <-time.After(2 * timeout):
assert.Fail(t, "readFirstPacket should have timed out")
}
})
t.Run("Incomplete payload timeout", func(t *testing.T) {
local, remote := connutil.AsyncPipe()
buf := make([]byte, 1500)
retChan := make(chan rfpReturnValue)
go rfp(remote, buf, retChan)
first, _ := hex.DecodeString("16030101010000")
local.Write(first)
select {
case ret := <-retChan:
assert.Equal(t, len(first), ret.n)
assert.False(t, ret.redirOnErr)
assert.Error(t, ret.err)
case <-time.After(2 * timeout):
assert.Fail(t, "readFirstPacket should have timed out")
}
})
t.Run("Good TLS staggered", func(t *testing.T) {
local, remote := connutil.AsyncPipe()
buf := make([]byte, 1500)
retChan := make(chan rfpReturnValue)
go rfp(remote, buf, retChan)
first, _ := hex.DecodeString("1603010200010001fc0303ac530b5778469dbbc3f9a83c6ac35b63aa6a70c2014026ade30f2faf0266f0242068424f320bcad49b4315a761f9f6dec32b0a403c2d8c0ab337608a694c6e411c0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00204655c2c83aaed1db2e89ed17d671fcdc76dc96e36bde8840022f1bda2f31019600170041543af1f8d28b37d984073f40e8361613da502f16e4039f00656f427de0f66480b2e77e3e552e126bb0cc097168f6e5454c7f9501126a2377fb40151f6cfc007e0e002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
local.Write(first[:100])
time.Sleep(timeout / 2)
local.Write(first[100:])
ret := <-retChan
assert.Equal(t, len(first), ret.n)
assert.Equal(t, first, buf[:ret.n])
assert.IsType(t, TLS{}, ret.transport)
assert.NoError(t, ret.err)
})
t.Run("Good WebSocket", func(t *testing.T) {
local, remote := connutil.AsyncPipe()
buf := make([]byte, 1500)
retChan := make(chan rfpReturnValue)
go rfp(remote, buf, retChan)
reqStr := "GET / HTTP/1.1\r\nHost: d2jkinvisak5y9.cloudfront.net:443\r\nUser-Agent: Go-http-client/1.1\r\nConnection: Upgrade\r\nHidden: oJxeEwfDWg5k5Jbl8ttZD1sc0fHp8VjEtXHsqEoSrnaLRe/M+KGXkOzpc/2fRRg9Vk+wIWRsfv8IpoBPLbqO+ZfGsPXTjUJGiI9BqxrcJfkxncXA7FAHGpTc84tzBtZZ\r\nSec-WebSocket-Key: lJYh7X8DRXW1U0h9WKwVMA==\r\nSec-WebSocket-Version: 13\r\nUpgrade: websocket\r\n\r\n"
req := []byte(reqStr)
local.Write(req)
ret := <-retChan
assert.Equal(t, len(req), ret.n)
assert.Equal(t, req, buf[:ret.n])
assert.IsType(t, WebSocket{}, ret.transport)
assert.NoError(t, ret.err)
})
t.Run("Good WebSocket but buf too small", func(t *testing.T) {
local, remote := connutil.AsyncPipe()
buf := make([]byte, 10)
retChan := make(chan rfpReturnValue)
go rfp(remote, buf, retChan)
reqStr := "GET / HTTP/1.1\r\nHost: d2jkinvisak5y9.cloudfront.net:443\r\nUser-Agent: Go-http-client/1.1\r\nConnection: Upgrade\r\nHidden: oJxeEwfDWg5k5Jbl8ttZD1sc0fHp8VjEtXHsqEoSrnaLRe/M+KGXkOzpc/2fRRg9Vk+wIWRsfv8IpoBPLbqO+ZfGsPXTjUJGiI9BqxrcJfkxncXA7FAHGpTc84tzBtZZ\r\nSec-WebSocket-Key: lJYh7X8DRXW1U0h9WKwVMA==\r\nSec-WebSocket-Version: 13\r\nUpgrade: websocket\r\n\r\n"
req := []byte(reqStr)
local.Write(req)
ret := <-retChan
assert.Equal(t, io.ErrShortBuffer, ret.err)
assert.True(t, ret.redirOnErr)
assert.Equal(t, req[:ret.n], buf[:ret.n])
})
t.Run("Incomplete WebSocket timeout", func(t *testing.T) {
local, remote := connutil.AsyncPipe()
buf := make([]byte, 1500)
retChan := make(chan rfpReturnValue)
go rfp(remote, buf, retChan)
reqStr := "GET /"
req := []byte(reqStr)
local.Write(req)
select {
case ret := <-retChan:
assert.Equal(t, len(req), ret.n)
assert.False(t, ret.redirOnErr)
assert.Error(t, ret.err)
case <-time.After(2 * timeout):
assert.Fail(t, "readFirstPacket should have timed out")
}
})
t.Run("Staggered WebSocket", func(t *testing.T) {
local, remote := connutil.AsyncPipe()
buf := make([]byte, 1500)
retChan := make(chan rfpReturnValue)
go rfp(remote, buf, retChan)
reqStr := "GET / HTTP/1.1\r\nHost: d2jkinvisak5y9.cloudfront.net:443\r\nUser-Agent: Go-http-client/1.1\r\nConnection: Upgrade\r\nHidden: oJxeEwfDWg5k5Jbl8ttZD1sc0fHp8VjEtXHsqEoSrnaLRe/M+KGXkOzpc/2fRRg9Vk+wIWRsfv8IpoBPLbqO+ZfGsPXTjUJGiI9BqxrcJfkxncXA7FAHGpTc84tzBtZZ\r\nSec-WebSocket-Key: lJYh7X8DRXW1U0h9WKwVMA==\r\nSec-WebSocket-Version: 13\r\nUpgrade: websocket\r\n\r\n"
req := []byte(reqStr)
local.Write(req[:100])
time.Sleep(timeout / 2)
local.Write(req[100:])
ret := <-retChan
assert.Equal(t, len(req), ret.n)
assert.Equal(t, req, buf[:ret.n])
assert.IsType(t, WebSocket{}, ret.transport)
assert.NoError(t, ret.err)
})
}
Loading…
Cancel
Save