Skip to content

Commit

Permalink
Shuffle ciphers
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna committed Dec 13, 2018
1 parent b942e4c commit 47a08be
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 35 deletions.
37 changes: 37 additions & 0 deletions shadowsocks/cipher_map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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
}
67 changes: 36 additions & 31 deletions shadowsocks/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func ensureBytes(reader io.Reader, buf []byte, bytesNeeded int) ([]byte, error)
return buf, err
}

func findAccessKey(clientConn onet.DuplexConn, cipherList map[string]shadowaead.Cipher) (string, onet.DuplexConn, error) {
func findAccessKey(clientConn onet.DuplexConn, cipherMap map[string]shadowaead.Cipher) (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.
Expand All @@ -57,11 +57,12 @@ func findAccessKey(clientConn onet.DuplexConn, cipherList map[string]shadowaead.
chunkLenBuf := [2]byte{}
var err error

// Try each cipher until we find one that authenticates successfully.
// This assumes that all ciphers are AEAD.
// 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.
// TODO: Ban and log client IPs with too many failures too quick to protect against DoS.
for id, cipher := range cipherList {
for _, entry := range shuffleCipherMap(cipherMap) {
id, cipher := entry.id, entry.cipher
replayBytes, err = ensureBytes(clientConn, replayBytes, cipher.SaltSize())
if err != nil {
if logger.IsEnabledFor(logging.DEBUG) {
Expand Down Expand Up @@ -114,6 +115,36 @@ type TCPService interface {
Stop() error
}

// proxyConnection will route the clientConn according to the address read from the connection.
func proxyConnection(clientConn onet.DuplexConn, proxyMetrics *metrics.ProxyMetrics) *onet.ConnectionError {
tgtAddr, err := socks.ReadAddr(clientConn)
if err != nil {
return onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", err)
}
tgtTCPAddr, err := net.ResolveTCPAddr("tcp", tgtAddr.String())
if err != nil {
return onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr.String()), err)
}
if !tgtTCPAddr.IP.IsGlobalUnicast() {
return onet.NewConnectionError("ERR_ADDRESS_INVALID", fmt.Sprintf("Target address is not global unicast: %v", tgtAddr.String()), err)
}

tgtTCPConn, err := net.DialTCP("tcp", nil, tgtTCPAddr)
if err != nil {
return onet.NewConnectionError("ERR_CONNECT", "Failed to connect to target", err)
}
defer tgtTCPConn.Close()
tgtTCPConn.SetKeepAlive(true)
tgtConn := metrics.MeasureConn(tgtTCPConn, &proxyMetrics.ProxyTarget, &proxyMetrics.TargetProxy)

logger.Debugf("proxy %s <-> %s", clientConn.RemoteAddr().String(), tgtConn.RemoteAddr().String())
_, _, err = onet.Relay(clientConn, tgtConn)
if err != nil {
return onet.NewConnectionError("ERR_RELAY", "Failed to relay traffic", err)
}
return nil
}

func (s *tcpService) Start() {
s.isRunning = true
for s.isRunning {
Expand Down Expand Up @@ -161,33 +192,7 @@ func (s *tcpService) Start() {
return onet.NewConnectionError("ERR_CIPHER", "Failed to find a valid cipher", err)
}

tgtAddr, err := socks.ReadAddr(clientConn)
if err != nil {
return onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", err)
}
tgtTCPAddr, err := net.ResolveTCPAddr("tcp", tgtAddr.String())
if err != nil {
return onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr.String()), err)
}
if !tgtTCPAddr.IP.IsGlobalUnicast() {
return onet.NewConnectionError("ERR_ADDRESS_INVALID", fmt.Sprintf("Target address is not global unicast: %v", tgtAddr.String()), err)
}

tgtTCPConn, err := net.DialTCP("tcp", nil, tgtTCPAddr)
if err != nil {
return onet.NewConnectionError("ERR_CONNECT", "Failed to connect to target", err)
}
defer tgtTCPConn.Close()
tgtTCPConn.SetKeepAlive(true)
tgtConn := metrics.MeasureConn(tgtTCPConn, &proxyMetrics.ProxyTarget, &proxyMetrics.TargetProxy)

// TODO: Disable logging in production. This is sensitive.
logger.Debugf("proxy %s <-> %s", clientConn.RemoteAddr().String(), tgtConn.RemoteAddr().String())
_, _, err = onet.Relay(clientConn, tgtConn)
if err != nil {
return onet.NewConnectionError("ERR_RELAY", "Failed to relay traffic", err)
}
return nil
return proxyConnection(clientConn, &proxyMetrics)
}()
}
}
Expand Down
11 changes: 7 additions & 4 deletions shadowsocks/udp.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +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, ciphers map[string]shadowaead.Cipher) ([]byte, string, shadowaead.Cipher, error) {
for id, cipher := range ciphers {
func unpack(dst, src []byte, cipherMap map[string]shadowaead.Cipher) ([]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
buf, err := shadowaead.Unpack(dst, src, cipher)
if err != nil {
if logger.IsEnabledFor(logging.DEBUG) {
Expand All @@ -61,8 +64,8 @@ type udpService struct {
}

// NewUDPService creates a UDPService
func NewUDPService(clientConn net.PacketConn, natTimeout time.Duration, ciphers *map[string]shadowaead.Cipher, m metrics.ShadowsocksMetrics) UDPService {
return &udpService{clientConn: clientConn, natTimeout: natTimeout, ciphers: ciphers, m: m}
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}
}

// UDPService is a UDP shadowsocks service that can be started and stopped.
Expand Down

0 comments on commit 47a08be

Please sign in to comment.