From 600bf0530974c1d0b4928a964ce8b7745065ade8 Mon Sep 17 00:00:00 2001 From: fortuna Date: Tue, 18 Dec 2018 16:35:41 -0500 Subject: [PATCH] Keep active ciphers at the front --- server.go | 24 +++--- shadowsocks/cipher_cache.go | 81 ------------------- shadowsocks/cipher_list.go | 69 ++++++++++++++++ shadowsocks/cipher_map.go | 37 --------- .../{testing/ciphers.go => cipher_testing.go} | 8 +- shadowsocks/tcp.go | 16 ++-- shadowsocks/tcp_test.go | 5 +- shadowsocks/udp.go | 16 ++-- shadowsocks/udp_test.go | 5 +- 9 files changed, 106 insertions(+), 155 deletions(-) delete mode 100644 shadowsocks/cipher_cache.go create mode 100644 shadowsocks/cipher_list.go delete mode 100644 shadowsocks/cipher_map.go rename shadowsocks/{testing/ciphers.go => cipher_testing.go} (85%) diff --git a/server.go b/server.go index 2097a0ce..0307b56d 100644 --- a/server.go +++ b/server.go @@ -54,7 +54,7 @@ func init() { type SSPort struct { tcpService shadowsocks.TCPService udpService shadowsocks.UDPService - keys map[string]shadowaead.Cipher + cipherList shadowsocks.CipherList } type SSServer struct { @@ -73,10 +73,10 @@ func (s *SSServer) startPort(portNum int) error { return fmt.Errorf("Failed to start UDP on port %v: %v", portNum, err) } logger.Infof("Listening TCP and UDP on port %v", portNum) - port := &SSPort{keys: make(map[string]shadowaead.Cipher)} + port := &SSPort{cipherList: shadowsocks.NewCipherList()} // TODO: Register initial data metrics at zero. - port.tcpService = shadowsocks.NewTCPService(listener, &port.keys, s.m) - port.udpService = shadowsocks.NewUDPService(packetConn, s.natTimeout, &port.keys, s.m) + port.tcpService = shadowsocks.NewTCPService(listener, &port.cipherList, s.m) + port.udpService = shadowsocks.NewUDPService(packetConn, s.natTimeout, &port.cipherList, s.m) s.ports[portNum] = port go port.udpService.Start() go port.tcpService.Start() @@ -108,13 +108,13 @@ func (s *SSServer) loadConfig(filename string) error { } portChanges := make(map[int]int) - portKeys := make(map[int]map[string]shadowaead.Cipher) + portCiphers := make(map[int]shadowsocks.CipherList) for _, keyConfig := range config.Keys { portChanges[keyConfig.Port] = 1 - keys, ok := portKeys[keyConfig.Port] + cipherList, ok := portCiphers[keyConfig.Port] if !ok { - keys = make(map[string]shadowaead.Cipher) - portKeys[keyConfig.Port] = keys + cipherList = shadowsocks.NewCipherList() + portCiphers[keyConfig.Port] = cipherList } cipher, err := core.PickCipher(keyConfig.Cipher, nil, keyConfig.Secret) if err != nil { @@ -127,7 +127,7 @@ func (s *SSServer) loadConfig(filename string) error { if !ok { return fmt.Errorf("Only AEAD ciphers are supported. Found %v", keyConfig.Cipher) } - keys[keyConfig.ID] = aead + cipherList.PushBack(keyConfig.ID, aead) } for port := range s.ports { portChanges[port] = portChanges[port] - 1 @@ -143,11 +143,11 @@ func (s *SSServer) loadConfig(filename string) error { } } } - for portNum, keys := range portKeys { - s.ports[portNum].keys = keys + for portNum, cipherList := range portCiphers { + s.ports[portNum].cipherList = cipherList } logger.Infof("Loaded %v access keys", len(config.Keys)) - s.m.SetNumAccessKeys(len(config.Keys), len(portKeys)) + s.m.SetNumAccessKeys(len(config.Keys), len(portCiphers)) return nil } diff --git a/shadowsocks/cipher_cache.go b/shadowsocks/cipher_cache.go deleted file mode 100644 index 7c8bd7bb..00000000 --- a/shadowsocks/cipher_cache.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2018 Jigsaw Operations LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package shadowsocks - -import ( - "container/list" - "time" -) - -type CipherCache struct { - ipCiphers map[string]*list.List -} - -type CachedItem struct { - itemsList *list.List - element *list.Element -} - -func (c *CipherCache) GetCiphers(ip string) []*CachedItem { - cipherList, ok := c.ipCiphers[ip] - if !ok { - return []*CachedItem{} - } - items := make([]*CachedItem, cipherList.Len()) - pos := 0 - for el := cipherList.Front(); el != nil; el = el.Next() { - items[pos] = &CachedItem{cipherList, el} - } - return items -} - -type cipherTime struct { - CipherID string - Timestamp time.Time -} - -// WARNING -// TODO: All of this needs a MUTEX!!!!!!! -// WARNING -func (cc *CipherCache) AddCipher(ip string, cipherId string) { - cipherList, ok := cc.ipCiphers[ip] - if !ok { - cipherList = list.New() - cc.ipCiphers[ip] = cipherList - } - cipherList.PushFront(cipherTime{CipherID: cipherId, Timestamp: time.Now()}) -} - -func (cc *CipherCache) ExpireOlderThan(oldestTime time.Time) { - for key, itemList := range cc.ipCiphers { - // Remove expired items - for item := itemList.Back(); item != nil && item.Value.(*cipherTime).Timestamp.Sub(oldestTime) < 0; item = itemList.Back() { - itemList.Remove(item) - } - if itemList.Len() == 0 { - // TODO: Make this not break the loop - delete(cc.ipCiphers, key) - } - } -} - -func (ci *CachedItem) Refresh() { - ci.element.Value.(*cipherTime).Timestamp = time.Now() - ci.itemsList.MoveToFront(ci.element) -} - -func (ci *CachedItem) CipherId() string { - return ci.element.Value.(*cipherTime).CipherID -} diff --git a/shadowsocks/cipher_list.go b/shadowsocks/cipher_list.go new file mode 100644 index 00000000..04e871c1 --- /dev/null +++ b/shadowsocks/cipher_list.go @@ -0,0 +1,69 @@ +// Copyright 2018 Jigsaw Operations LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shadowsocks + +import ( + "container/list" + "sync" + + "github.com/shadowsocks/go-shadowsocks2/shadowaead" +) + +// CipherEntry holds a Cipher with an identifier. +type CipherEntry struct { + ID string + Cipher shadowaead.Cipher +} + +// CipherList is a list of CipherEntry elements that allows for thread-safe snapshotting and +// moving to front. +type CipherList interface { + PushBack(id string, cipher shadowaead.Cipher) *list.Element + SafeSnapshot() []*list.Element + SafeMoveToFront(e *list.Element) +} + +type cipherList struct { + CipherList + list *list.List + mu sync.RWMutex +} + +// NewCipherList creates an empty CipherList +func NewCipherList() CipherList { + return &cipherList{list: list.New()} +} + +func (cl *cipherList) PushBack(id string, cipher shadowaead.Cipher) *list.Element { + return cl.list.PushBack(&CipherEntry{ID: id, Cipher: cipher}) +} + +func (cl *cipherList) SafeSnapshot() []*list.Element { + cl.mu.RLock() + defer cl.mu.RUnlock() + cipherArray := make([]*list.Element, cl.list.Len()) + i := 0 + for e := cl.list.Front(); e != nil; e = e.Next() { + cipherArray[i] = e + i++ + } + return cipherArray +} + +func (cl *cipherList) SafeMoveToFront(e *list.Element) { + cl.mu.Lock() + defer cl.mu.Unlock() + cl.list.MoveToFront(e) +} diff --git a/shadowsocks/cipher_map.go b/shadowsocks/cipher_map.go deleted file mode 100644 index f63e5577..00000000 --- a/shadowsocks/cipher_map.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2018 Jigsaw Operations LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package shadowsocks - -import ( - "math/rand" - - "github.com/shadowsocks/go-shadowsocks2/shadowaead" -) - -type cipherEntry struct { - id string - cipher shadowaead.Cipher -} - -func shuffleCipherMap(cipherMap map[string]shadowaead.Cipher) []cipherEntry { - cipherArray := make([]cipherEntry, len(cipherMap)) - perm := rand.Perm(len(cipherMap)) - i := 0 - for id, cipher := range cipherMap { - cipherArray[perm[i]] = cipherEntry{id, cipher} - i++ - } - return cipherArray -} diff --git a/shadowsocks/testing/ciphers.go b/shadowsocks/cipher_testing.go similarity index 85% rename from shadowsocks/testing/ciphers.go rename to shadowsocks/cipher_testing.go index c8cbda3c..a2e8a80e 100644 --- a/shadowsocks/testing/ciphers.go +++ b/shadowsocks/cipher_testing.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package testing +package shadowsocks import ( "fmt" @@ -21,8 +21,8 @@ import ( "github.com/shadowsocks/go-shadowsocks2/shadowaead" ) -func MakeTestCiphers(numCiphers int) (map[string]shadowaead.Cipher, error) { - cipherList := make(map[string]shadowaead.Cipher) +func MakeTestCiphers(numCiphers int) (CipherList, error) { + cipherList := NewCipherList() for i := 0; i < numCiphers; i++ { cipherID := fmt.Sprintf("id-%v", i) secret := fmt.Sprintf("secret-%v", i) @@ -30,7 +30,7 @@ func MakeTestCiphers(numCiphers int) (map[string]shadowaead.Cipher, error) { if err != nil { return nil, fmt.Errorf("Failed to create cipher %v: %v", i, err) } - cipherList[cipherID] = cipher.(shadowaead.Cipher) + cipherList.PushBack(cipherID, cipher.(shadowaead.Cipher)) } return cipherList, nil } diff --git a/shadowsocks/tcp.go b/shadowsocks/tcp.go index 981fd767..6adee2d0 100644 --- a/shadowsocks/tcp.go +++ b/shadowsocks/tcp.go @@ -26,7 +26,6 @@ import ( "github.com/Jigsaw-Code/outline-ss-server/metrics" onet "github.com/Jigsaw-Code/outline-ss-server/net" - "github.com/shadowsocks/go-shadowsocks2/shadowaead" "github.com/shadowsocks/go-shadowsocks2/socks" ) @@ -48,7 +47,7 @@ func ensureBytes(reader io.Reader, buf []byte, bytesNeeded int) ([]byte, error) return buf, err } -func findAccessKey(clientConn onet.DuplexConn, cipherMap map[string]shadowaead.Cipher) (string, onet.DuplexConn, error) { +func findAccessKey(clientConn onet.DuplexConn, cipherList CipherList) (string, onet.DuplexConn, error) { // This must have enough space to hold the salt + 2 bytes chunk length + AEAD tag (Oeverhead) for any cipher replayBytes := make([]byte, 0, 32+2+16) // Constant of zeroes to use as the start chunk count. This must be as big as the max NonceSize() across all ciphers. @@ -58,11 +57,10 @@ func findAccessKey(clientConn onet.DuplexConn, cipherMap map[string]shadowaead.C var err error // Try each cipher until we find one that authenticates successfully. This assumes that all ciphers are AEAD. - // We shuffle the cipher map so that every connection has the same expected time. - // TODO: Reorder list to try previously successful ciphers first for the client IP. + // We snapshot the list because it may be modified while we use it. // TODO: Ban and log client IPs with too many failures too quick to protect against DoS. - for _, entry := range shuffleCipherMap(cipherMap) { - id, cipher := entry.id, entry.cipher + for _, entry := range cipherList.SafeSnapshot() { + id, cipher := entry.Value.(*CipherEntry).ID, entry.Value.(*CipherEntry).Cipher replayBytes, err = ensureBytes(clientConn, replayBytes, cipher.SaltSize()) if err != nil { if logger.IsEnabledFor(logging.DEBUG) { @@ -90,6 +88,8 @@ func findAccessKey(clientConn onet.DuplexConn, cipherMap map[string]shadowaead.C if logger.IsEnabledFor(logging.DEBUG) { logger.Debugf("Selected TCP cipher %v", id) } + // Move the active cipher to the front, so that the search is quicker next time. + cipherList.SafeMoveToFront(entry) ssr := NewShadowsocksReader(io.MultiReader(bytes.NewReader(replayBytes), clientConn), cipher) ssw := NewShadowsocksWriter(clientConn, cipher) return id, onet.WrapConn(clientConn, ssr, ssw).(onet.DuplexConn), nil @@ -99,13 +99,13 @@ func findAccessKey(clientConn onet.DuplexConn, cipherMap map[string]shadowaead.C type tcpService struct { listener *net.TCPListener - ciphers *map[string]shadowaead.Cipher + ciphers *CipherList m metrics.ShadowsocksMetrics isRunning bool } // NewTCPService creates a TCPService -func NewTCPService(listener *net.TCPListener, ciphers *map[string]shadowaead.Cipher, m metrics.ShadowsocksMetrics) TCPService { +func NewTCPService(listener *net.TCPListener, ciphers *CipherList, m metrics.ShadowsocksMetrics) TCPService { return &tcpService{listener: listener, ciphers: ciphers, m: m} } diff --git a/shadowsocks/tcp_test.go b/shadowsocks/tcp_test.go index 4a67febe..40f7b2d6 100644 --- a/shadowsocks/tcp_test.go +++ b/shadowsocks/tcp_test.go @@ -18,7 +18,6 @@ import ( "net" "testing" - sstest "github.com/Jigsaw-Code/outline-ss-server/shadowsocks/testing" logging "github.com/op/go-logging" ) @@ -32,11 +31,11 @@ func BenchmarkTCPFindCipher(b *testing.B) { b.Fatalf("ListenTCP failed: %v", err) } - cipherList, err := sstest.MakeTestCiphers(100) + cipherList, err := MakeTestCiphers(100) if err != nil { b.Fatal(err) } - testPayload := sstest.MakeTestPayload(50) + testPayload := MakeTestPayload(50) for n := 0; n < b.N; n++ { go func() { conn, err := net.Dial("tcp", listener.Addr().String()) diff --git a/shadowsocks/udp.go b/shadowsocks/udp.go index 8129efe7..a16244d7 100644 --- a/shadowsocks/udp.go +++ b/shadowsocks/udp.go @@ -35,11 +35,11 @@ const udpBufSize = 64 * 1024 // upack decrypts src into dst. It tries each cipher until it finds one that authenticates // correctly. dst and src must not overlap. -func unpack(dst, src []byte, cipherMap map[string]shadowaead.Cipher) ([]byte, string, shadowaead.Cipher, error) { +func unpack(dst, src []byte, cipherList CipherList) ([]byte, string, shadowaead.Cipher, error) { // Try each cipher until we find one that authenticates successfully. This assumes that all ciphers are AEAD. - // We shuffle the cipher map so that every connection has the same expected time. - for _, entry := range shuffleCipherMap(cipherMap) { - id, cipher := entry.id, entry.cipher + // We snapshot the list because it may be modified while we use it. + for _, entry := range cipherList.SafeSnapshot() { + id, cipher := entry.Value.(*CipherEntry).ID, entry.Value.(*CipherEntry).Cipher buf, err := shadowaead.Unpack(dst, src, cipher) if err != nil { if logger.IsEnabledFor(logging.DEBUG) { @@ -50,6 +50,8 @@ func unpack(dst, src []byte, cipherMap map[string]shadowaead.Cipher) ([]byte, st if logger.IsEnabledFor(logging.DEBUG) { logger.Debugf("Selected UDP cipher %v", id) } + // Move the active cipher to the front, so that the search is quicker next time. + cipherList.SafeMoveToFront(entry) return buf, id, cipher, nil } return nil, "", nil, errors.New("could not find valid cipher") @@ -58,14 +60,14 @@ func unpack(dst, src []byte, cipherMap map[string]shadowaead.Cipher) ([]byte, st type udpService struct { clientConn net.PacketConn natTimeout time.Duration - ciphers *map[string]shadowaead.Cipher + ciphers *CipherList m metrics.ShadowsocksMetrics isRunning bool } // NewUDPService creates a UDPService -func NewUDPService(clientConn net.PacketConn, natTimeout time.Duration, cipherMap *map[string]shadowaead.Cipher, m metrics.ShadowsocksMetrics) UDPService { - return &udpService{clientConn: clientConn, natTimeout: natTimeout, ciphers: cipherMap, m: m} +func NewUDPService(clientConn net.PacketConn, natTimeout time.Duration, cipherList *CipherList, m metrics.ShadowsocksMetrics) UDPService { + return &udpService{clientConn: clientConn, natTimeout: natTimeout, ciphers: cipherList, m: m} } // UDPService is a UDP shadowsocks service that can be started and stopped. diff --git a/shadowsocks/udp_test.go b/shadowsocks/udp_test.go index df903483..28992685 100644 --- a/shadowsocks/udp_test.go +++ b/shadowsocks/udp_test.go @@ -17,18 +17,17 @@ package shadowsocks import ( "testing" - sstest "github.com/Jigsaw-Code/outline-ss-server/shadowsocks/testing" logging "github.com/op/go-logging" ) func BenchmarkUDPUnpack(b *testing.B) { logging.SetLevel(logging.INFO, "") - cipherList, err := sstest.MakeTestCiphers(100) + cipherList, err := MakeTestCiphers(100) if err != nil { b.Fatal(err) } - testPayload := sstest.MakeTestPayload(50) + testPayload := MakeTestPayload(50) textBuf := make([]byte, udpBufSize) b.ResetTimer() for n := 0; n < b.N; n++ {