Skip to content

Commit

Permalink
Use the shadowsocks cipher to mark the salt
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Schwartz committed Aug 18, 2020
1 parent 1c62281 commit 1c6ebb5
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 34 deletions.
5 changes: 0 additions & 5 deletions shadowsocks/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,6 @@ func TestTCPEcho(t *testing.T) {
t.Fatal("Echo mismatch")
}

// Check for client and server salts.
if len(replayCache.active) != 2 {
t.Fatalf("Replay cache has wrong number of salts: %d", len(replayCache.active))
}

conn.Close()
proxy.Stop()
echoListener.Close()
Expand Down
80 changes: 70 additions & 10 deletions shadowsocks/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,81 @@ const payloadSizeMask = 0x3FFF // 16*1024 - 1

// SaltGenerator generates unique salts to use in Shadowsocks connections.
type SaltGenerator interface {
// Writes a new salt into the input slice
GetSalt(salt []byte) error
// Returns a new salt
GetSalt() ([]byte, error)
}

// randomSaltGenerator generates a new random salt.
type randomSaltGenerator struct{}
type randomSaltGenerator struct {
saltSize int
}

// ServerSaltGenerator generates unique salts that are secretly marked.
type ServerSaltGenerator struct {
saltSize int
encrypter cipher.AEAD
}

// GetSalt outputs a random salt.
func (*randomSaltGenerator) GetSalt(salt []byte) error {
func (sg randomSaltGenerator) GetSalt() ([]byte, error) {
salt := make([]byte, sg.saltSize)
_, err := io.ReadFull(rand.Reader, salt)
return err
return salt, err
}

// Number of bytes of salt to use as a marker.
const markLen = 4

// Constant to identify this marking scheme.
var serverIndication = []byte("outline-salt-mark")

func NewServerSaltGenerator(cipher shadowaead.Cipher) (ServerSaltGenerator, error) {
saltSize := cipher.SaltSize()
zerosalt := make([]byte, saltSize)
encrypter, err := cipher.Encrypter(zerosalt)
if err != nil {
return ServerSaltGenerator{}, err
}
return ServerSaltGenerator{saltSize, encrypter}, nil
}

func (sg ServerSaltGenerator) splitSalt(salt []byte) (prefix, mark []byte) {
prefixLen := sg.saltSize - markLen
prefix = salt[:prefixLen]
mark = salt[prefixLen:]
return
}

// RandomSaltGenerator is a SaltGenerator that generates a new random salt.
var RandomSaltGenerator SaltGenerator = &randomSaltGenerator{}
// getTag takes in a salt prefix and writes out the tag.
// len(prefix) must be saltSize - markLen
func (sg ServerSaltGenerator) getTag(prefix []byte) []byte {
nonce := make([]byte, sg.encrypter.NonceSize())
n := copy(nonce, prefix)
plaintext := prefix[n:]
encrypted := sg.encrypter.Seal(nil, nonce, plaintext, serverIndication)
return encrypted[len(plaintext):]
}

// GetSalt returns an apparently random salt that can be identified
// as server-originated by anyone who knows the Shadowsocks key.
func (sg ServerSaltGenerator) GetSalt() ([]byte, error) {
salt := make([]byte, sg.saltSize)
prefix, mark := sg.splitSalt(salt)
_, err := io.ReadFull(rand.Reader, prefix)
if err != nil {
return nil, err
}
tag := sg.getTag(prefix)
copy(mark, tag)
return salt, nil
}

// IsMarked returns true if the salt is marked as server-originated.
func (sg ServerSaltGenerator) IsMarked(salt []byte) bool {
prefix, mark := sg.splitSalt(salt)
tag := sg.getTag(prefix)
return bytes.Equal(tag[:markLen], mark)
}

// Writer is an io.Writer that also implements io.ReaderFrom to
// allow for piping the data without extra allocations and copies.
Expand Down Expand Up @@ -76,7 +136,7 @@ type Writer struct {
// NewShadowsocksWriter creates a Writer that encrypts the given Writer using
// the shadowsocks protocol with the given shadowsocks cipher.
func NewShadowsocksWriter(writer io.Writer, ssCipher shadowaead.Cipher) *Writer {
return &Writer{writer: writer, ssCipher: ssCipher, saltGenerator: RandomSaltGenerator}
return &Writer{writer: writer, ssCipher: ssCipher, saltGenerator: randomSaltGenerator{ssCipher.SaltSize()}}
}

// SetSaltGenerator sets the salt generator to be used. Must be called before the first write.
Expand All @@ -88,8 +148,8 @@ func (sw *Writer) SetSaltGenerator(saltGenerator SaltGenerator) {
// the salt to the inner Writer.
func (sw *Writer) init() (err error) {
if sw.aead == nil {
salt := make([]byte, sw.ssCipher.SaltSize())
if err := sw.saltGenerator.GetSalt(salt); err != nil {
salt, err := sw.saltGenerator.GetSalt()
if err != nil {
return fmt.Errorf("failed to generate salt: %v", err)
}
sw.aead, err = sw.ssCipher.Encrypter(salt)
Expand Down
38 changes: 19 additions & 19 deletions shadowsocks/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,6 @@ func debugTCP(cipherID, template string, val interface{}) {
}
}

type recordingSaltGenerator struct {
saltGenerator SaltGenerator
replayCache *ReplayCache
keyID string
}

func (sg *recordingSaltGenerator) GetSalt(salt []byte) error {
err := sg.saltGenerator.GetSalt(salt)
if err != nil {
return err
}
_ = sg.replayCache.Add(sg.keyID, salt)
return nil
}

func findAccessKey(clientReader io.Reader, clientIP net.IP, cipherList CipherList) (string, shadowaead.Cipher, io.Reader, []byte, time.Duration, error) {
// We snapshot the list because it may be modified while we use it.
tcpTrialSize, ciphers := cipherList.SnapshotForClientIP(clientIP)
Expand Down Expand Up @@ -255,18 +240,33 @@ func (s *tcpService) handleConnection(listenerPort int, clientConn onet.DuplexCo
const status = "ERR_CIPHER"
s.absorbProbe(listenerPort, clientConn, clientLocation, status, &proxyMetrics)
return onet.NewConnectionError(status, "Failed to find a valid cipher", keyErr)
} else if !s.replayCache.Add(keyID, salt) { // Only check the cache if findAccessKey succeeded.
}

saltGenerator, err := NewServerSaltGenerator(cipher)
if err != nil {
return onet.NewConnectionError("ERR_SALTGEN", "Failed to construct salt generator", err)
}

isMarked := saltGenerator.IsMarked(salt)
// Only check the cache if findAccessKey succeeded and the salt is unmarked.
if isMarked || !s.replayCache.Add(keyID, salt) {
const status = "ERR_REPLAY"
s.absorbProbe(listenerPort, clientConn, clientLocation, status, &proxyMetrics)
logger.Debugf("Replay: %v in %s sent %d bytes", clientConn.RemoteAddr(), clientLocation, proxyMetrics.ClientProxy)
return onet.NewConnectionError(status, "Replay detected", nil)
var msg string
if isMarked {
msg = "Server replay detected"
} else {
msg = "Client replay detected"
}
logger.Debugf(msg+": %v in %s sent %d bytes", clientConn.RemoteAddr(), clientLocation, proxyMetrics.ClientProxy)
return onet.NewConnectionError(status, msg, nil)
}
// Clear the authentication deadline
clientConn.SetReadDeadline(time.Time{})

ssr := NewShadowsocksReader(clientReader, cipher)
ssw := NewShadowsocksWriter(clientConn, cipher)
ssw.SetSaltGenerator(&recordingSaltGenerator{saltGenerator: RandomSaltGenerator, replayCache: s.replayCache, keyID: keyID})
ssw.SetSaltGenerator(saltGenerator)
clientConn = onet.WrapConn(clientConn, ssr, ssw)
return proxyConnection(clientConn, &proxyMetrics, s.checkAllowedIP)
}()
Expand Down

0 comments on commit 1c6ebb5

Please sign in to comment.