Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent replays of server data #78

Merged
merged 15 commits into from
Aug 27, 2020
Prev Previous commit
Next Next commit
Use the shadowsocks cipher to mark the salt
  • Loading branch information
Ben Schwartz committed Aug 18, 2020
commit 1c6ebb57b039d929cd15ed3df9177f3149428ab7
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 {
bemasc marked this conversation as resolved.
Show resolved Hide resolved
saltSize int
encrypter cipher.AEAD
}

// GetSalt outputs a random salt.
func (*randomSaltGenerator) GetSalt(salt []byte) error {
func (sg randomSaltGenerator) GetSalt() ([]byte, error) {
bemasc marked this conversation as resolved.
Show resolved Hide resolved
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)
bemasc marked this conversation as resolved.
Show resolved Hide resolved
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 {
bemasc marked this conversation as resolved.
Show resolved Hide resolved
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)
bemasc marked this conversation as resolved.
Show resolved Hide resolved
// Only check the cache if findAccessKey succeeded and the salt is unmarked.
if isMarked || !s.replayCache.Add(keyID, salt) {
const status = "ERR_REPLAY"
fortuna marked this conversation as resolved.
Show resolved Hide resolved
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)
bemasc marked this conversation as resolved.
Show resolved Hide resolved
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