Skip to content

Commit 25f6db2

Browse files
author
Benjamin M. Schwartz
authored
Merge pull request #90 from Jigsaw-Code/bemasc-pool2
Reduce memory usage in read streams
2 parents 72e7ee4 + 523b106 commit 25f6db2

File tree

2 files changed

+42
-11
lines changed

2 files changed

+42
-11
lines changed

shadowsocks/cipher.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ func newAesGCM(key []byte) (cipher.AEAD, error) {
7777
return cipher.NewGCM(blk)
7878
}
7979

80+
func maxTagSize() int {
81+
max := 0
82+
for _, spec := range supportedAEADs {
83+
if spec.tagSize > max {
84+
max = spec.tagSize
85+
}
86+
}
87+
return max
88+
}
89+
8090
// Cipher encapsulates a Shadowsocks AEAD spec and a secret
8191
type Cipher struct {
8292
aead aeadSpec

shadowsocks/stream.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ import (
2121
"fmt"
2222
"io"
2323
"sync"
24+
25+
"github.com/Jigsaw-Code/outline-ss-server/slicepool"
2426
)
2527

2628
// payloadSizeMask is the maximum size of payload in bytes.
2729
const payloadSizeMask = 0x3FFF // 16*1024 - 1
2830

31+
// Buffer pool used for decrypting Shadowsocks streams.
32+
// The largest buffer we could need is for decrypting a max-length payload.
33+
var readBufPool = slicepool.MakePool(payloadSizeMask + maxTagSize())
34+
2935
// Writer is an io.Writer that also implements io.ReaderFrom to
3036
// allow for piping the data without extra allocations and copies.
3137
// The LazyWrite and Flush methods allow a header to be
@@ -262,7 +268,10 @@ type chunkReader struct {
262268
aead cipher.AEAD
263269
// Index of the next encrypted chunk to read.
264270
counter []byte
265-
buf []byte
271+
// Buffer for the uint16 size and its AEAD tag. Made in init().
272+
payloadSizeBuf []byte
273+
// Holds a buffer for the payload and its AEAD tag, when needed.
274+
payload slicepool.LazySlice
266275
}
267276

268277
// Reader is an io.Reader that also implements io.WriterTo to
@@ -276,7 +285,11 @@ type Reader interface {
276285
// the shadowsocks protocol with the given shadowsocks cipher.
277286
func NewShadowsocksReader(reader io.Reader, ssCipher *Cipher) Reader {
278287
return &readConverter{
279-
cr: &chunkReader{reader: reader, ssCipher: ssCipher},
288+
cr: &chunkReader{
289+
reader: reader,
290+
ssCipher: ssCipher,
291+
payload: readBufPool.LazySlice(),
292+
},
280293
}
281294
}
282295

@@ -296,7 +309,7 @@ func (cr *chunkReader) init() (err error) {
296309
return fmt.Errorf("failed to create AEAD: %v", err)
297310
}
298311
cr.counter = make([]byte, cr.aead.NonceSize())
299-
cr.buf = make([]byte, payloadSizeMask+cr.aead.Overhead())
312+
cr.payloadSizeBuf = make([]byte, 2+cr.aead.Overhead())
300313
}
301314
return nil
302315
}
@@ -318,31 +331,38 @@ func (cr *chunkReader) readMessage(buf []byte) error {
318331
return nil
319332
}
320333

334+
// ReadChunk returns the next chunk from the stream. Callers must fully
335+
// consume and discard the previous chunk before calling ReadChunk again.
321336
func (cr *chunkReader) ReadChunk() ([]byte, error) {
322337
if err := cr.init(); err != nil {
323338
return nil, err
324339
}
340+
341+
// Release the previous payload buffer.
342+
cr.payload.Release()
343+
325344
// In Shadowsocks-AEAD, each chunk consists of two
326345
// encrypted messages. The first message contains the payload length,
327-
// and the second message is the payload.
328-
sizeBuf := cr.buf[:2+cr.aead.Overhead()]
329-
if err := cr.readMessage(sizeBuf); err != nil {
346+
// and the second message is the payload. Idle read threads will
347+
// block here until the next chunk.
348+
if err := cr.readMessage(cr.payloadSizeBuf); err != nil {
330349
if err != io.EOF && err != io.ErrUnexpectedEOF {
331350
err = fmt.Errorf("failed to read payload size: %v", err)
332351
}
333352
return nil, err
334353
}
335-
size := int(binary.BigEndian.Uint16(sizeBuf) & payloadSizeMask)
354+
size := int(binary.BigEndian.Uint16(cr.payloadSizeBuf) & payloadSizeMask)
336355
sizeWithTag := size + cr.aead.Overhead()
337-
if cap(cr.buf) < sizeWithTag {
338-
// This code is unreachable.
356+
payloadBuf := cr.payload.Acquire()
357+
if cap(payloadBuf) < sizeWithTag {
358+
// This code is unreachable if the constants are set correctly.
339359
return nil, io.ErrShortBuffer
340360
}
341-
payloadBuf := cr.buf[:sizeWithTag]
342-
if err := cr.readMessage(payloadBuf); err != nil {
361+
if err := cr.readMessage(payloadBuf[:sizeWithTag]); err != nil {
343362
if err == io.EOF { // EOF is not expected mid-chunk.
344363
err = io.ErrUnexpectedEOF
345364
}
365+
cr.payload.Release()
346366
return nil, err
347367
}
348368
return payloadBuf[:size], nil
@@ -388,6 +408,7 @@ func (c *readConverter) ensureLeftover() error {
388408
if len(c.leftover) > 0 {
389409
return nil
390410
}
411+
c.leftover = nil
391412
payload, err := c.cr.ReadChunk()
392413
if err != nil {
393414
return err

0 commit comments

Comments
 (0)