Compare commits

...

24 commits

Author SHA1 Message Date
Shell.Xu
e332ec93e9 use list instead of string, prevent injection attack. (#1009)
* fix issue:
https://github.com/shadowsocks/shadowsocks/issues/995
Command Execution

use list instead of string, prevent injection attack.
2018-02-19 10:42:32 +08:00
netroby
c668f44c68 Update README.md (#1042) 2018-02-19 01:22:57 +08:00
mengskysama
8b302646ed
update run_method() (#1161) 2018-02-19 01:08:52 +08:00
mengskysama
2ab8c6bf5d Update jenkins.sh
rm CI coverage
2017-09-06 16:11:14 +08:00
Andrew Lyu
f7d7aee3a6 Fix typo (#866) 2017-09-06 15:21:13 +08:00
mengskysama
0168be7c92 Update .travis.yml
new path for nginx static file
2017-09-06 15:19:45 +08:00
Anthony Wong
0b0379b830 Add new file snapcraft.yaml for building snap package (#932) 2017-09-06 15:13:39 +08:00
Anthony Wong
91e6237794 Update README.md (#734)
Add installation and usage instructions for snap package.
2017-09-06 15:13:26 +08:00
Minwei Shen
3c82f02343 Improve lru_cache. Update test for it (#897) 2017-09-06 15:12:37 +08:00
Esdeath
893f9099b3 Fix memory leak (#921)
* Fix memory leak

* Fix memory leak(2)

* refine
2017-09-06 15:07:19 +08:00
Ted George
d5026cf5ef Update README.md (#841)
thanks
2017-05-12 16:07:36 +08:00
Zou Yong
13b5d6e5ec Fix udp dns issue under python3 (#849)
str is not bytes under python3, if dns config is a list, will cause asyncdns.py failed:

asyncdns.py:
..........
    def handle_event(self, sock, fd, event):
        if sock != self._sock:
            return
        if event & eventloop.POLL_ERR:
       ...........
        else:
            data, addr = sock.recvfrom(1024)
            if addr[0] not in self._servers:
                logging.warn('received a packet other than our dns')
                return
            self._handle_data(data)
2017-05-12 16:06:38 +08:00
Zou Yong
06b028b5c0 Fix memory leak when excpetions occur (#814) 2017-04-29 14:29:53 +08:00
Zou Yong
0f4e3fa00c Add mbedtls wrapper, custom crypto lib path, test files (#803)
* add mbedtls crypto wrapper.
add tests files for new aead ciphers
add custom lib path support
fix some typo

* fix forbidden ip list

* rm crypto lib build files

* remove crypto source

* add xchacha20 test config

* convert dos new line format to unix format

* Fix help msg
2017-04-29 14:27:55 +08:00
Falseen
1222fb19a6 update find_lib's name for the new openssl-1.1 dll (just for windows) (#796)
the new openssl's dll(1.1) is rename to libcrypto-1_1-x64.dll and libcrypto-1_1.dll
2017-03-21 20:15:38 +08:00
Falseen
9e25cc6bb4 fix some error for parse dns_servre in config (#798)
and fix pep8

https://github.com/shadowsocks/shadowsocks/pull/739
2017-03-19 12:37:57 +08:00
Zou Yong
4e21f83bd6 Handle cipher exceptions #783 (#791) 2017-03-15 00:35:17 +08:00
Zou Yong
74f8f8cb85 Add sodium:aes-256-gcm and openssl:ocb AEAD mode (#784)
* Add sodium:aes-256-gcm and openssl:ocb AEAD mode
Use sodium_increment for nonce_increment if avaiable

* Fix pep8

* Fix python3 test code
2017-03-10 17:44:42 +08:00
Zou Yong
d6b40efa5d encrypt.py is renamed to cryptor.py (#780) 2017-03-05 01:09:40 +08:00
Zou Yong
445a3c9c7e Add AEAD ciphers support (#775)
* Add AEAD ciphers support, add manger api

* fix test_encrypt_all

* Add manager api requirements

* #775 fix UDP decrypt_all issue

* fix udp replay: decrypt_all return a list
remove manager api

* fix indent according pep8

* remove abc requirement

* remove unused import

* fix pep8 format

* fix test_aes_256_gcm()
2017-03-04 14:37:29 +08:00
Elvis Wang
c4731de532 Update README.md (#575)
The shadowsocks in pypi is out of date, may install from github@master
2017-03-02 21:28:16 +08:00
Falseen
e06819c124 add simple ss-tunnel to shadowsocks for dns forward (#759)
* add vscode to .gitignore

* add config.json to gitignore

* add simple ss-tunnel to shadowsocks for dns forward


1.add tunnel.py file to shadowoscks for dns forward (seem ss-tunnel of ss-libev)

2.add add_header to common.py for add socks5 request header

3.add dns_service dns_server dns_server_port dns_server_port dns_local_port to shell.py and config.json

4.update to udprelay for ss-tunnel (dns forward)

5.update to local.py for ss-tunnel

* add config.json.example to shadowsocks

* add tunnel_udp_server.close to local.py and tunnel.py

* fix error about pep8 and  pyflakes for travis

* update rename

rename

add "tunnel_" to dns_service dns_server dns_server_port dns_local_port

* fix tunnel for run tunnel alone

* fix pep8 for travis

* update the config name for tunnel

ord name:
tunnel_service tunnel_dns_server tunnel_dns_server_port tunnel_dns_local_port

new name:
both_tunnel_local tunnel_remote tunnel_remote_port tunnel_port

* update for tunnel

update to local.py shell.py tunnel.py for tunnel

* update pep8 for travis

* update config to _config for tunnel

and fix pep8

* changed "add socks5 header to data" to "add ss header to data"

and changed "remove socks5 header" to "remove ss header"

* add tcp forward to tunnel and fix a bug for pack_addr from common.py

1. update tunnel to tcprelay.py

2. add tunnel_tcp_server to tunnel.py

3. add tunnel_tcp_server to local.py

4. add `address = to_bytes(address)` to `pack_addr` from common.py (fix a error when address is a domain)

* fix pep8 for travis again

* remove ss_header from tcprelay.py and update the "header_length" to udprelay.py

1. Remove unnecessary "add ss_header" from tcprelay.py

2. update "data[7:]" to "data[header_length:]"

* remove "both_tunnel_local" and fix some error for tunnel

* update

* update add_header

* rename is_tunnel to _is_tunnel

https://github.com/shadowsocks/shadowsocks/pull/759
2017-03-02 21:20:04 +08:00
ImPerat0R_
fe13c20dc1 Correct spelling. (#755)
thx
2017-02-28 22:19:36 +08:00
hdl
8ac72b0b25 parse dns_servre in config (#739)
thx
2017-02-28 22:15:47 +08:00
57 changed files with 2473 additions and 466 deletions

11
.gitignore vendored
View file

@ -29,6 +29,17 @@ htmlcov
.DS_Store
.idea
tags
#Emacs
.#*
venv/
# VS-code
.vscode/
# Pycharm
.idea
#ss
config.json

View file

@ -10,12 +10,14 @@ cache:
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq build-essential dnsutils iproute nginx bc
- sudo dd if=/dev/urandom of=/usr/share/nginx/www/file bs=1M count=10
- sudo dd if=/dev/urandom of=/usr/share/nginx/html/file bs=1M count=10
- sudo sh -c "echo '127.0.0.1 localhost' > /etc/hosts"
- sudo service nginx restart
- pip install pep8 pyflakes nose coverage PySocks
- sudo tests/socksify/install.sh
- sudo tests/libsodium/install.sh
- sudo tests/libmbedtls/install.sh
- sudo tests/libopenssl/install.sh
- sudo tests/setup_tc.sh
script:
- tests/jenkins.sh

View file

@ -3,7 +3,6 @@ shadowsocks
[![PyPI version]][PyPI]
[![Build Status]][Travis CI]
[![Coverage Status]][Coverage]
A fast tunnel proxy that helps you bypass firewalls.
@ -22,12 +21,21 @@ Server
Debian / Ubuntu:
apt-get install python-pip
pip install shadowsocks
pip install git+https://github.com/shadowsocks/shadowsocks.git@master
CentOS:
yum install python-setuptools && easy_install pip
pip install shadowsocks
pip install git+https://github.com/shadowsocks/shadowsocks.git@master
For CentOS 7, if you need AEAD ciphers, you need install libsodium
```
dnf install libsodium python34-pip
pip3 install git+https://github.com/shadowsocks/shadowsocks.git@master
```
Linux distributions with [snap](http://snapcraft.io/):
snap install shadowsocks
Windows:
@ -52,9 +60,14 @@ To check the log:
Check all the options via `-h`. You can also use a [Configuration] file
instead.
If you installed the [snap](http://snapcraft.io/) package, you have to prefix the commands with `shadowsocks.`,
like this:
shadowsocks.ssserver -p 443 -k password -m aes-256-cfb
### Usage with Config File
[Create configeration file and run](https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File)
[Create configuration file and run](https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File)
To start:
@ -78,8 +91,6 @@ Apache License
[Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat
[Coverage Status]: https://jenkins.shadowvpn.org/result/shadowsocks
[Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html
[PyPI]: https://pypi.python.org/pypi/shadowsocks
[PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat
[Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks

17
config.json.example Normal file
View file

@ -0,0 +1,17 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1080,
"password":"password",
"timeout":600,
"method":"aes-256-cfb",
"local_address":"127.0.0.1",
"fast_open":false,
"tunnel_remote":"8.8.8.8",
"dns_server":["8.8.8.8", "8.8.4.4"],
"tunnel_remote_port":53,
"tunnel_port":53,
"libopenssl":"C:\\Program Files\\Git\\mingw64\\bin\\libeay32.dll",
"libsodium":"/usr/local/lib/libsodium.so",
"libmbedtls":"/usr/local/lib/libmbedcrypto.2.4.0.dylib"
}

View file

@ -7,7 +7,7 @@ with codecs.open('README.rst', encoding='utf-8') as f:
setup(
name="shadowsocks",
version="2.9.1",
version="3.0.0",
license='http://www.apache.org/licenses/LICENSE-2.0',
description="A fast tunnel proxy that help you get through firewalls",
author='clowwindy',

View file

@ -146,6 +146,7 @@ ADDRTYPE_MASK = 0xF
def pack_addr(address):
address_str = to_str(address)
address = to_bytes(address)
for family in (socket.AF_INET, socket.AF_INET6):
try:
r = socket.inet_pton(family, address_str)
@ -160,6 +161,13 @@ def pack_addr(address):
return b'\x03' + chr(len(address)) + address
# add ss header
def add_header(address, port, data=b''):
_data = b''
_data = pack_addr(address) + struct.pack('>H', port) + data
return _data
def parse_header(data):
addrtype = ord(data[0])
dest_addr = None

340
shadowsocks/crypto/aead.py Normal file
View file

@ -0,0 +1,340 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Void Copyright NO ONE
#
# Void License
#
# The code belongs to no one. Do whatever you want.
# Forget about boring open source license.
#
# AEAD cipher for shadowsocks
#
from __future__ import absolute_import, division, print_function, \
with_statement
from ctypes import c_int, create_string_buffer, byref, c_void_p
import hashlib
from struct import pack, unpack
from shadowsocks.crypto import util
from shadowsocks.crypto import hkdf
from shadowsocks.common import ord, chr
EVP_CTRL_GCM_SET_IVLEN = 0x9
EVP_CTRL_GCM_GET_TAG = 0x10
EVP_CTRL_GCM_SET_TAG = 0x11
EVP_CTRL_CCM_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN
EVP_CTRL_CCM_GET_TAG = EVP_CTRL_GCM_GET_TAG
EVP_CTRL_CCM_SET_TAG = EVP_CTRL_GCM_SET_TAG
EVP_CTRL_AEAD_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN
EVP_CTRL_AEAD_SET_TAG = EVP_CTRL_GCM_SET_TAG
EVP_CTRL_AEAD_GET_TAG = EVP_CTRL_GCM_GET_TAG
AEAD_MSG_LEN_UNKNOWN = 0
AEAD_CHUNK_SIZE_LEN = 2
AEAD_CHUNK_SIZE_MASK = 0x3FFF
CIPHER_NONCE_LEN = {
'aes-128-gcm': 12,
'aes-192-gcm': 12,
'aes-256-gcm': 12,
'aes-128-ocb': 12, # requires openssl 1.1
'aes-192-ocb': 12,
'aes-256-ocb': 12,
'chacha20-poly1305': 12,
'chacha20-ietf-poly1305': 12,
'xchacha20-ietf-poly1305': 24,
'sodium:aes-256-gcm': 12,
}
CIPHER_TAG_LEN = {
'aes-128-gcm': 16,
'aes-192-gcm': 16,
'aes-256-gcm': 16,
'aes-128-ocb': 16, # requires openssl 1.1
'aes-192-ocb': 16,
'aes-256-ocb': 16,
'chacha20-poly1305': 16,
'chacha20-ietf-poly1305': 16,
'xchacha20-ietf-poly1305': 16,
'sodium:aes-256-gcm': 16,
}
SUBKEY_INFO = b"ss-subkey"
libsodium = None
sodium_loaded = False
def load_sodium(path=None):
"""
Load libsodium helpers for nonce increment
:return: None
"""
global libsodium, sodium_loaded
libsodium = util.find_library('sodium', 'sodium_increment',
'libsodium', path)
if libsodium is None:
print('load libsodium failed with path %s' % path)
return
if libsodium.sodium_init() < 0:
libsodium = None
print('sodium init failed')
return
libsodium.sodium_increment.restype = c_void_p
libsodium.sodium_increment.argtypes = (
c_void_p, c_int
)
sodium_loaded = True
return
def nonce_increment(nonce, nlen):
"""
Increase nonce by 1 in little endian
From libsodium sodium_increment():
for (; i < nlen; i++) {
c += (uint_fast16_t) n[i];
n[i] = (unsigned char) c;
c >>= 8;
}
:param nonce: string_buffer nonce
:param nlen: nonce length
:return: nonce plus by 1
"""
c = 1
i = 0
# n = create_string_buffer(nlen)
while i < nlen:
c += ord(nonce[i])
nonce[i] = chr(c & 0xFF)
c >>= 8
i += 1
return # n.raw
class AeadCryptoBase(object):
"""
Handles basic aead process of shadowsocks protocol
TCP Chunk (after encryption, *ciphertext*)
+--------------+---------------+--------------+------------+
| *DataLen* | DataLen_TAG | *Data* | Data_TAG |
+--------------+---------------+--------------+------------+
| 2 | Fixed | Variable | Fixed |
+--------------+---------------+--------------+------------+
UDP (after encryption, *ciphertext*)
+--------+-----------+-----------+
| NONCE | *Data* | Data_TAG |
+-------+-----------+-----------+
| Fixed | Variable | Fixed |
+--------+-----------+-----------+
"""
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
self._op = int(op)
self._salt = iv
self._nlen = CIPHER_NONCE_LEN[cipher_name]
self._nonce = create_string_buffer(self._nlen)
self._tlen = CIPHER_TAG_LEN[cipher_name]
crypto_hkdf = hkdf.Hkdf(iv, key, algorithm=hashlib.sha1)
self._skey = crypto_hkdf.expand(info=SUBKEY_INFO, length=len(key))
# _chunk['mlen']:
# -1, waiting data len header
# n, n > 0, waiting data
self._chunk = {'mlen': AEAD_MSG_LEN_UNKNOWN, 'data': b''}
# load libsodium for nonce increment
if not sodium_loaded:
crypto_path = dict(crypto_path) if crypto_path else dict()
path = crypto_path.get('sodium', None)
load_sodium(path)
def nonce_increment(self):
"""
AEAD ciphers need nonce to be unique per key
TODO: cache and check unique
:return: None
"""
global libsodium, sodium_loaded
if sodium_loaded:
libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen))
else:
nonce_increment(self._nonce, self._nlen)
# print("".join("%02x" % ord(b) for b in self._nonce))
def cipher_ctx_init(self):
"""
Increase nonce to make it unique for the same key
:return: None
"""
self.nonce_increment()
def aead_encrypt(self, data):
"""
Encrypt data with authenticate tag
:param data: plain text
:return: str [payload][tag] cipher text with tag
"""
raise Exception("Must implement aead_encrypt method")
def encrypt_chunk(self, data):
"""
Encrypt a chunk for TCP chunks
:param data: str
:return: str [len][tag][payload][tag]
"""
plen = len(data)
# l = AEAD_CHUNK_SIZE_LEN + plen + self._tlen * 2
# network byte order
ctext = [self.aead_encrypt(pack("!H", plen & AEAD_CHUNK_SIZE_MASK))]
if len(ctext[0]) != AEAD_CHUNK_SIZE_LEN + self._tlen:
self.clean()
raise Exception("size length invalid")
ctext.append(self.aead_encrypt(data))
if len(ctext[1]) != plen + self._tlen:
self.clean()
raise Exception("data length invalid")
return b''.join(ctext)
def encrypt(self, data):
"""
Encrypt data, for TCP divided into chunks
For UDP data, call aead_encrypt instead
:param data: str data bytes
:return: str encrypted data
"""
plen = len(data)
if plen <= AEAD_CHUNK_SIZE_MASK:
ctext = self.encrypt_chunk(data)
return ctext
ctext = []
while plen > 0:
mlen = plen if plen < AEAD_CHUNK_SIZE_MASK \
else AEAD_CHUNK_SIZE_MASK
c = self.encrypt_chunk(data[:mlen])
ctext.append(c)
data = data[mlen:]
plen -= mlen
return b''.join(ctext)
def aead_decrypt(self, data):
"""
Decrypt data and authenticate tag
:param data: str [len][tag][payload][tag] cipher text with tag
:return: str plain text
"""
raise Exception("Must implement aead_decrypt method")
def decrypt_chunk_size(self, data):
"""
Decrypt chunk size
:param data: str [size][tag] encrypted chunk payload len
:return: (int, str) msg length and remaining encrypted data
"""
if self._chunk['mlen'] > 0:
return self._chunk['mlen'], data
data = self._chunk['data'] + data
self._chunk['data'] = b""
hlen = AEAD_CHUNK_SIZE_LEN + self._tlen
if hlen > len(data):
self._chunk['data'] = data
return 0, b""
plen = self.aead_decrypt(data[:hlen])
plen, = unpack("!H", plen)
if plen & AEAD_CHUNK_SIZE_MASK != plen or plen <= 0:
self.clean()
raise Exception('Invalid message length')
return plen, data[hlen:]
def decrypt_chunk_payload(self, plen, data):
"""
Decrypted encrypted msg payload
:param plen: int payload length
:param data: str [payload][tag][[len][tag]....] encrypted data
:return: (str, str) plain text and remaining encrypted data
"""
data = self._chunk['data'] + data
if len(data) < plen + self._tlen:
self._chunk['mlen'] = plen
self._chunk['data'] = data
return b"", b""
self._chunk['mlen'] = AEAD_MSG_LEN_UNKNOWN
self._chunk['data'] = b""
plaintext = self.aead_decrypt(data[:plen + self._tlen])
if len(plaintext) != plen:
self.clean()
raise Exception("plaintext length invalid")
return plaintext, data[plen + self._tlen:]
def decrypt_chunk(self, data):
"""
Decrypt a TCP chunk
:param data: str [len][tag][payload][tag][[len][tag]...] encrypted msg
:return: (str, str) decrypted msg and remaining encrypted data
"""
plen, data = self.decrypt_chunk_size(data)
if plen <= 0:
return b"", b""
return self.decrypt_chunk_payload(plen, data)
def decrypt(self, data):
"""
Decrypt data for TCP data divided into chunks
For UDP data, call aead_decrypt instead
:param data: str
:return: str
"""
ptext = []
pnext, left = self.decrypt_chunk(data)
ptext.append(pnext)
while len(left) > 0:
pnext, left = self.decrypt_chunk(left)
ptext.append(pnext)
return b''.join(ptext)
def test_nonce_increment():
buf = create_string_buffer(12)
print("".join("%02x" % ord(b) for b in buf))
nonce_increment(buf, 12)
nonce_increment(buf, 12)
nonce_increment(buf, 12)
nonce_increment(buf, 12)
print("".join("%02x" % ord(b) for b in buf))
for i in range(256):
nonce_increment(buf, 12)
print("".join("%02x" % ord(b) for b in buf))
if __name__ == '__main__':
load_sodium()
test_nonce_increment()

View file

@ -0,0 +1,98 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Void Copyright NO ONE
#
# Void License
#
# The code belongs to no one. Do whatever you want.
# Forget about boring open source license.
#
# HKDF for AEAD ciphers
#
from __future__ import division
import hmac
import hashlib
import sys
if sys.version_info[0] == 3:
def buffer(x):
return x
def hkdf_extract(salt, input_key_material, algorithm=hashlib.sha256):
"""
Extract a pseudorandom key suitable for use with hkdf_expand
from the input_key_material and a salt using HMAC with the
provided hash (default SHA-256).
salt should be a random, application-specific byte string. If
salt is None or the empty string, an all-zeros string of the same
length as the hash's block size will be used instead per the RFC.
See the HKDF draft RFC and paper for usage notes.
"""
hash_len = algorithm().digest_size
if salt is None or len(salt) == 0:
salt = bytearray((0,) * hash_len)
return hmac.new(bytes(salt), buffer(input_key_material), algorithm)\
.digest()
def hkdf_expand(pseudo_random_key, info=b"", length=32,
algorithm=hashlib.sha256):
"""
Expand `pseudo_random_key` and `info` into a key of length `bytes` using
HKDF's expand function based on HMAC with the provided hash (default
SHA-256). See the HKDF draft RFC and paper for usage notes.
"""
hash_len = algorithm().digest_size
length = int(length)
if length > 255 * hash_len:
raise Exception("Cannot expand to more than 255 * %d = %d "
"bytes using the specified hash function" %
(hash_len, 255 * hash_len))
blocks_needed = length // hash_len \
+ (0 if length % hash_len == 0 else 1) # ceil
okm = b""
output_block = b""
for counter in range(blocks_needed):
output_block = hmac.new(
pseudo_random_key,
buffer(output_block + info + bytearray((counter + 1,))),
algorithm
).digest()
okm += output_block
return okm[:length]
class Hkdf(object):
"""
Wrapper class for HKDF extract and expand functions
"""
def __init__(self, salt, input_key_material, algorithm=hashlib.sha256):
"""
Extract a pseudorandom key from `salt` and `input_key_material`
arguments.
See the HKDF draft RFC for guidance on setting these values.
The constructor optionally takes a `algorithm` argument defining
the hash function use, defaulting to hashlib.sha256.
"""
self._hash = algorithm
self._prk = hkdf_extract(salt, input_key_material, self._hash)
def expand(self, info, length=32):
"""
Generate output key material based on an `info` value
Arguments:
- info - context to generate the OKM
- length - length in bytes of the key to generate
See the HKDF draft RFC for guidance.
"""
return hkdf_expand(self._prk, info, length, self._hash)

View file

@ -0,0 +1,478 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Void Copyright NO ONE
#
# Void License
#
# The code belongs to no one. Do whatever you want.
# Forget about boring open source license.
#
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
from __future__ import absolute_import, division, print_function, \
with_statement
from ctypes import c_char_p, c_int, c_size_t, byref,\
create_string_buffer, c_void_p
from shadowsocks import common
from shadowsocks.crypto import util
from shadowsocks.crypto.aead import AeadCryptoBase
__all__ = ['ciphers']
libmbedtls = None
loaded = False
buf = None
buf_size = 2048
CIPHER_ENC_UNCHANGED = -1
# define MAX_KEY_LENGTH 64
# define MAX_NONCE_LENGTH 32
# typedef struct {
# uint32_t init;
# uint64_t counter;
# cipher_evp_t *evp;
# cipher_t *cipher;
# buffer_t *chunk;
# uint8_t salt[MAX_KEY_LENGTH];
# uint8_t skey[MAX_KEY_LENGTH];
# uint8_t nonce[MAX_NONCE_LENGTH];
# } cipher_ctx_t;
#
# sizeof(cipher_ctx_t) = 196
CIPHER_CTX_SIZE = 256
def load_mbedtls(crypto_path=None):
global loaded, libmbedtls, buf
crypto_path = dict(crypto_path) if crypto_path else dict()
path = crypto_path.get('mbedtls', None)
libmbedtls = util.find_library('mbedcrypto',
'mbedtls_cipher_init',
'libmbedcrypto', path)
if libmbedtls is None:
raise Exception('libmbedcrypto(mbedtls) not found with path %s'
% path)
libmbedtls.mbedtls_cipher_init.restype = None
libmbedtls.mbedtls_cipher_free.restype = None
libmbedtls.mbedtls_cipher_info_from_string.restype = c_void_p
libmbedtls.mbedtls_cipher_info_from_string.argtypes = (c_char_p,)
libmbedtls.mbedtls_cipher_setup.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_setup.argtypes = (c_void_p, c_void_p)
libmbedtls.mbedtls_cipher_setkey.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_setkey.argtypes = (
c_void_p, # ctx
c_char_p, # key
c_int, # key_bitlen, not bytes
c_int # op: 1 enc, 0 dec, -1 none
)
libmbedtls.mbedtls_cipher_set_iv.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_set_iv.argtypes = (
c_void_p, # ctx
c_char_p, # iv
c_size_t # iv_len
)
libmbedtls.mbedtls_cipher_reset.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_reset.argtypes = (c_void_p,) # ctx
if hasattr(libmbedtls, 'mbedtls_cipher_update_ad'):
libmbedtls.mbedtls_cipher_update_ad.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_update_ad.argtypes = (
c_void_p, # ctx
c_char_p, # ad
c_size_t # ad_len
)
libmbedtls.mbedtls_cipher_update.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_update.argtypes = (
c_void_p, # ctx
c_char_p, # input
c_size_t, # ilen, must be multiple of block size except last one
c_void_p, # *output
c_void_p # *olen
)
libmbedtls.mbedtls_cipher_finish.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_finish.argtypes = (
c_void_p, # ctx
c_void_p, # *output
c_void_p # *olen
)
if hasattr(libmbedtls, 'mbedtls_cipher_write_tag'):
libmbedtls.mbedtls_cipher_write_tag.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_write_tag.argtypes = (
c_void_p, # ctx
c_void_p, # *tag
c_size_t # tag_len
)
libmbedtls.mbedtls_cipher_check_tag.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_check_tag.argtypes = (
c_void_p, # ctx
c_char_p, # tag
c_size_t # tag_len
)
libmbedtls.mbedtls_cipher_crypt.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_crypt.argtypes = (
c_void_p, # ctx
c_char_p, # iv
c_size_t, # iv_len, = 0 if iv = NULL
c_char_p, # input
c_size_t, # ilen
c_void_p, # *output, no less than ilen + block_size
c_void_p # *olen
)
if hasattr(libmbedtls, 'mbedtls_cipher_auth_encrypt'):
libmbedtls.mbedtls_cipher_auth_encrypt.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_auth_encrypt.argtypes = (
c_void_p, # ctx
c_char_p, # iv
c_size_t, # iv_len
c_char_p, # ad
c_size_t, # ad_len
c_char_p, # input
c_size_t, # ilen
c_void_p, # *output, no less than ilen + block_size
c_void_p, # *olen
c_void_p, # *tag
c_size_t # tag_len
)
libmbedtls.mbedtls_cipher_auth_decrypt.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_auth_decrypt.argtypes = (
c_void_p, # ctx
c_char_p, # iv
c_size_t, # iv_len
c_char_p, # ad
c_size_t, # ad_len
c_char_p, # input
c_size_t, # ilen
c_void_p, # *output, no less than ilen + block_size
c_void_p, # *olen
c_char_p, # tag
c_size_t, # tag_len
)
buf = create_string_buffer(buf_size)
loaded = True
class MbedTLSCryptoBase(object):
"""
MbedTLS crypto base class
"""
def __init__(self, cipher_name, crypto_path=None):
global loaded
self._ctx = create_string_buffer(b'\0' * CIPHER_CTX_SIZE)
self._cipher = None
if not loaded:
load_mbedtls(crypto_path)
cipher_name = common.to_bytes(cipher_name.upper())
cipher = libmbedtls.mbedtls_cipher_info_from_string(cipher_name)
if not cipher:
raise Exception('cipher %s not found in libmbedtls' % cipher_name)
libmbedtls.mbedtls_cipher_init(byref(self._ctx))
if libmbedtls.mbedtls_cipher_setup(byref(self._ctx), cipher):
raise Exception('can not setup cipher')
self._cipher = cipher
self.encrypt_once = self.update
self.decrypt_once = self.update
def update(self, data):
"""
Encrypt/decrypt data
:param data: str
:return: str
"""
global buf_size, buf
cipher_out_len = c_size_t(0)
l = len(data)
if buf_size < l:
buf_size = l * 2
buf = create_string_buffer(buf_size)
libmbedtls.mbedtls_cipher_update(
byref(self._ctx),
c_char_p(data), c_size_t(l),
byref(buf), byref(cipher_out_len)
)
# buf is copied to a str object when we access buf.raw
return buf.raw[:cipher_out_len.value]
def __del__(self):
self.clean()
def clean(self):
if self._ctx:
libmbedtls.mbedtls_cipher_free(byref(self._ctx))
class MbedTLSAeadCrypto(MbedTLSCryptoBase, AeadCryptoBase):
"""
Implement mbedtls Aead mode: gcm
"""
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
if cipher_name[:len('mbedtls:')] == 'mbedtls:':
cipher_name = cipher_name[len('mbedtls:'):]
MbedTLSCryptoBase.__init__(self, cipher_name, crypto_path)
AeadCryptoBase.__init__(self, cipher_name, key, iv, op, crypto_path)
key_ptr = c_char_p(self._skey)
r = libmbedtls.mbedtls_cipher_setkey(
byref(self._ctx),
key_ptr, c_int(len(key) * 8),
c_int(op)
)
if r:
self.clean()
raise Exception('can not initialize cipher context')
r = libmbedtls.mbedtls_cipher_reset(byref(self._ctx))
if r:
self.clean()
raise Exception('can not finish preparation of mbed TLS '
'cipher context')
def cipher_ctx_init(self):
"""
Nonce + 1
:return: None
"""
AeadCryptoBase.nonce_increment(self)
def set_tag(self, tag):
"""
Set tag before decrypt any data (update)
:param tag: authenticated tag
:return: None
"""
tag_len = self._tlen
r = libmbedtls.mbedtls_cipher_check_tag(
byref(self._ctx),
c_char_p(tag), c_size_t(tag_len)
)
if not r:
raise Exception('Set tag failed')
def get_tag(self):
"""
Get authenticated tag, called after EVP_CipherFinal_ex
:return: str
"""
tag_len = self._tlen
tag_buf = create_string_buffer(tag_len)
r = libmbedtls.mbedtls_cipher_write_tag(
byref(self._ctx),
byref(tag_buf), c_size_t(tag_len)
)
if not r:
raise Exception('Get tag failed')
return tag_buf.raw[:tag_len]
def final(self):
"""
Finish encrypt/decrypt a chunk (<= 0x3FFF)
:return: str
"""
global buf_size, buf
cipher_out_len = c_size_t(0)
r = libmbedtls.mbedtls_cipher_finish(
byref(self._ctx),
byref(buf), byref(cipher_out_len)
)
if not r:
# print(self._nonce.raw, r, cipher_out_len)
raise Exception('Finalize cipher failed')
return buf.raw[:cipher_out_len.value]
def aead_encrypt(self, data):
"""
Encrypt data with authenticate tag
:param data: plain text
:return: cipher text with tag
"""
global buf_size, buf
plen = len(data)
if buf_size < plen + self._tlen:
buf_size = (plen + self._tlen) * 2
buf = create_string_buffer(buf_size)
cipher_out_len = c_size_t(0)
tag_buf = create_string_buffer(self._tlen)
r = libmbedtls.mbedtls_cipher_auth_encrypt(
byref(self._ctx),
c_char_p(self._nonce.raw), c_size_t(self._nlen),
None, c_size_t(0),
c_char_p(data), c_size_t(plen),
byref(buf), byref(cipher_out_len),
byref(tag_buf), c_size_t(self._tlen)
)
assert cipher_out_len.value == plen
if r:
raise Exception('AEAD encrypt failed {0:#x}'.format(r))
self.cipher_ctx_init()
return buf.raw[:cipher_out_len.value] + tag_buf.raw[:self._tlen]
def aead_decrypt(self, data):
"""
Decrypt data and authenticate tag
:param data: cipher text with tag
:return: plain text
"""
global buf_size, buf
cipher_out_len = c_size_t(0)
plen = len(data) - self._tlen
if buf_size < plen:
buf_size = plen * 2
buf = create_string_buffer(buf_size)
tag = data[plen:]
r = libmbedtls.mbedtls_cipher_auth_decrypt(
byref(self._ctx),
c_char_p(self._nonce.raw), c_size_t(self._nlen),
None, c_size_t(0),
c_char_p(data), c_size_t(plen),
byref(buf), byref(cipher_out_len),
c_char_p(tag), c_size_t(self._tlen)
)
if r:
raise Exception('AEAD encrypt failed {0:#x}'.format(r))
self.cipher_ctx_init()
return buf.raw[:cipher_out_len.value]
class MbedTLSStreamCrypto(MbedTLSCryptoBase):
"""
Crypto for stream modes: cfb, ofb, ctr
"""
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
if cipher_name[:len('mbedtls:')] == 'mbedtls:':
cipher_name = cipher_name[len('mbedtls:'):]
MbedTLSCryptoBase.__init__(self, cipher_name, crypto_path)
key_ptr = c_char_p(key)
iv_ptr = c_char_p(iv)
r = libmbedtls.mbedtls_cipher_setkey(
byref(self._ctx),
key_ptr, c_int(len(key) * 8),
c_int(op)
)
if r:
self.clean()
raise Exception('can not set cipher key')
r = libmbedtls.mbedtls_cipher_set_iv(
byref(self._ctx),
iv_ptr, c_size_t(len(iv))
)
if r:
self.clean()
raise Exception('can not set cipher iv')
r = libmbedtls.mbedtls_cipher_reset(byref(self._ctx))
if r:
self.clean()
raise Exception('can not reset cipher')
self.encrypt = self.update
self.decrypt = self.update
ciphers = {
'mbedtls:aes-128-cfb128': (16, 16, MbedTLSStreamCrypto),
'mbedtls:aes-192-cfb128': (24, 16, MbedTLSStreamCrypto),
'mbedtls:aes-256-cfb128': (32, 16, MbedTLSStreamCrypto),
'mbedtls:aes-128-ctr': (16, 16, MbedTLSStreamCrypto),
'mbedtls:aes-192-ctr': (24, 16, MbedTLSStreamCrypto),
'mbedtls:aes-256-ctr': (32, 16, MbedTLSStreamCrypto),
'mbedtls:camellia-128-cfb128': (16, 16, MbedTLSStreamCrypto),
'mbedtls:camellia-192-cfb128': (24, 16, MbedTLSStreamCrypto),
'mbedtls:camellia-256-cfb128': (32, 16, MbedTLSStreamCrypto),
# AEAD: iv_len = salt_len = key_len
'mbedtls:aes-128-gcm': (16, 16, MbedTLSAeadCrypto),
'mbedtls:aes-192-gcm': (24, 24, MbedTLSAeadCrypto),
'mbedtls:aes-256-gcm': (32, 32, MbedTLSAeadCrypto),
}
def run_method(method):
print(method, ': [stream]', 32)
cipher = MbedTLSStreamCrypto(method, b'k' * 32, b'i' * 16, 1)
decipher = MbedTLSStreamCrypto(method, b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher)
def run_aead_method(method, key_len=16):
print(method, ': [payload][tag]', key_len)
key_len = int(key_len)
cipher = MbedTLSAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1)
decipher = MbedTLSAeadCrypto(
method,
b'k' * key_len, b'i' * key_len, 0
)
util.run_cipher(cipher, decipher)
def run_aead_method_chunk(method, key_len=16):
print(method, ': chunk([size][tag][payload][tag]', key_len)
key_len = int(key_len)
cipher = MbedTLSAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1)
decipher = MbedTLSAeadCrypto(
method,
b'k' * key_len, b'i' * key_len, 0
)
cipher.encrypt_once = cipher.encrypt
decipher.decrypt_once = decipher.decrypt
util.run_cipher(cipher, decipher)
def test_camellia_256_cfb():
run_method('camellia-256-cfb128')
def test_aes_gcm(bits=128):
method = "aes-{0}-gcm".format(bits)
run_aead_method(method, bits / 8)
def test_aes_gcm_chunk(bits=128):
method = "aes-{0}-gcm".format(bits)
run_aead_method_chunk(method, bits / 8)
def test_aes_256_cfb():
run_method('aes-256-cfb128')
def test_aes_256_ctr():
run_method('aes-256-ctr')
if __name__ == '__main__':
test_aes_256_cfb()
test_camellia_256_cfb()
test_aes_256_ctr()
test_aes_gcm(128)
test_aes_gcm(192)
test_aes_gcm(256)
test_aes_gcm_chunk(128)
test_aes_gcm_chunk(192)
test_aes_gcm_chunk(256)

View file

@ -22,33 +22,46 @@ from ctypes import c_char_p, c_int, c_long, byref,\
from shadowsocks import common
from shadowsocks.crypto import util
from shadowsocks.crypto.aead import AeadCryptoBase, EVP_CTRL_AEAD_SET_IVLEN, \
EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG
__all__ = ['ciphers']
libcrypto = None
loaded = False
libsodium = None
buf = None
buf_size = 2048
ctx_cleanup = None
def load_openssl():
global loaded, libcrypto, buf, ctx_cleanup
CIPHER_ENC_UNCHANGED = -1
def load_openssl(crypto_path=None):
global loaded, libcrypto, libsodium, buf, ctx_cleanup
crypto_path = dict(crypto_path) if crypto_path else dict()
path = crypto_path.get('openssl', None)
libcrypto = util.find_library(('crypto', 'eay32'),
'EVP_get_cipherbyname',
'libcrypto')
'libcrypto', path)
if libcrypto is None:
raise Exception('libcrypto(OpenSSL) not found')
raise Exception('libcrypto(OpenSSL) not found with path %s' % path)
libcrypto.EVP_get_cipherbyname.restype = c_void_p
libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p
libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p,
c_char_p, c_char_p, c_int)
libcrypto.EVP_CIPHER_CTX_ctrl.argtypes = (c_void_p, c_int, c_int, c_void_p)
libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p,
c_char_p, c_int)
libcrypto.EVP_CipherFinal_ex.argtypes = (c_void_p, c_void_p, c_void_p)
try:
libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
ctx_cleanup = libcrypto.EVP_CIPHER_CTX_cleanup
@ -64,7 +77,7 @@ def load_openssl():
def load_cipher(cipher_name):
func_name = 'EVP_' + cipher_name.replace('-', '_')
func_name = b'EVP_' + cipher_name.replace(b'-', b'_')
if bytes != str:
func_name = str(func_name, 'utf-8')
cipher = getattr(libcrypto, func_name, None)
@ -74,37 +87,48 @@ def load_cipher(cipher_name):
return None
class OpenSSLCrypto(object):
def __init__(self, cipher_name, key, iv, op):
class OpenSSLCryptoBase(object):
"""
OpenSSL crypto base class
"""
def __init__(self, cipher_name, crypto_path=None):
self._ctx = None
self._cipher = None
if not loaded:
load_openssl()
load_openssl(crypto_path)
cipher_name = common.to_bytes(cipher_name)
cipher = libcrypto.EVP_get_cipherbyname(cipher_name)
if not cipher:
cipher = load_cipher(cipher_name)
if not cipher:
raise Exception('cipher %s not found in libcrypto' % cipher_name)
key_ptr = c_char_p(key)
iv_ptr = c_char_p(iv)
self._ctx = libcrypto.EVP_CIPHER_CTX_new()
self._cipher = cipher
if not self._ctx:
raise Exception('can not create cipher context')
r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None,
key_ptr, iv_ptr, c_int(op))
if not r:
self.clean()
raise Exception('can not initialize cipher context')
def encrypt_once(self, data):
return self.update(data)
def decrypt_once(self, data):
return self.update(data)
def update(self, data):
"""
Encrypt/decrypt data
:param data: str
:return: str
"""
global buf_size, buf
cipher_out_len = c_long(0)
l = len(data)
if buf_size < l:
buf_size = l * 2
buf = create_string_buffer(buf_size)
libcrypto.EVP_CipherUpdate(self._ctx, byref(buf),
byref(cipher_out_len), c_char_p(data), l)
libcrypto.EVP_CipherUpdate(
self._ctx, byref(buf),
byref(cipher_out_len), c_char_p(data), l
)
# buf is copied to a str object when we access buf.raw
return buf.raw[:cipher_out_len.value]
@ -115,45 +139,269 @@ class OpenSSLCrypto(object):
if self._ctx:
ctx_cleanup(self._ctx)
libcrypto.EVP_CIPHER_CTX_free(self._ctx)
self._ctx = None
class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase):
"""
Implement OpenSSL Aead mode: gcm, ocb
"""
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
OpenSSLCryptoBase.__init__(self, cipher_name, crypto_path)
AeadCryptoBase.__init__(self, cipher_name, key, iv, op, crypto_path)
key_ptr = c_char_p(self._skey)
r = libcrypto.EVP_CipherInit_ex(
self._ctx,
self._cipher,
None,
key_ptr, None,
c_int(op)
)
if not r:
self.clean()
raise Exception('can not initialize cipher context')
r = libcrypto.EVP_CIPHER_CTX_ctrl(
self._ctx,
c_int(EVP_CTRL_AEAD_SET_IVLEN),
c_int(self._nlen),
None
)
if not r:
self.clean()
raise Exception('Set ivlen failed')
self.cipher_ctx_init()
def cipher_ctx_init(self):
"""
Need init cipher context after EVP_CipherFinal_ex to reuse context
:return: None
"""
iv_ptr = c_char_p(self._nonce.raw)
r = libcrypto.EVP_CipherInit_ex(
self._ctx,
None,
None,
None, iv_ptr,
c_int(CIPHER_ENC_UNCHANGED)
)
if not r:
self.clean()
raise Exception('can not initialize cipher context')
AeadCryptoBase.nonce_increment(self)
def set_tag(self, tag):
"""
Set tag before decrypt any data (update)
:param tag: authenticated tag
:return: None
"""
tag_len = self._tlen
r = libcrypto.EVP_CIPHER_CTX_ctrl(
self._ctx,
c_int(EVP_CTRL_AEAD_SET_TAG),
c_int(tag_len), c_char_p(tag)
)
if not r:
self.clean()
raise Exception('Set tag failed')
def get_tag(self):
"""
Get authenticated tag, called after EVP_CipherFinal_ex
:return: str
"""
tag_len = self._tlen
tag_buf = create_string_buffer(tag_len)
r = libcrypto.EVP_CIPHER_CTX_ctrl(
self._ctx,
c_int(EVP_CTRL_AEAD_GET_TAG),
c_int(tag_len), byref(tag_buf)
)
if not r:
self.clean()
raise Exception('Get tag failed')
return tag_buf.raw[:tag_len]
def final(self):
"""
Finish encrypt/decrypt a chunk (<= 0x3FFF)
:return: str
"""
global buf_size, buf
cipher_out_len = c_long(0)
r = libcrypto.EVP_CipherFinal_ex(
self._ctx,
byref(buf), byref(cipher_out_len)
)
if not r:
self.clean()
# print(self._nonce.raw, r, cipher_out_len)
raise Exception('Finalize cipher failed')
return buf.raw[:cipher_out_len.value]
def aead_encrypt(self, data):
"""
Encrypt data with authenticate tag
:param data: plain text
:return: cipher text with tag
"""
ctext = self.update(data) + self.final() + self.get_tag()
self.cipher_ctx_init()
return ctext
def aead_decrypt(self, data):
"""
Decrypt data and authenticate tag
:param data: cipher text with tag
:return: plain text
"""
clen = len(data)
if clen < self._tlen:
self.clean()
raise Exception('Data too short')
self.set_tag(data[clen - self._tlen:])
plaintext = self.update(data[:clen - self._tlen]) + self.final()
self.cipher_ctx_init()
return plaintext
def encrypt_once(self, data):
return self.aead_encrypt(data)
def decrypt_once(self, data):
return self.aead_decrypt(data)
class OpenSSLStreamCrypto(OpenSSLCryptoBase):
"""
Crypto for stream modes: cfb, ofb, ctr
"""
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
OpenSSLCryptoBase.__init__(self, cipher_name, crypto_path)
key_ptr = c_char_p(key)
iv_ptr = c_char_p(iv)
r = libcrypto.EVP_CipherInit_ex(self._ctx, self._cipher, None,
key_ptr, iv_ptr, c_int(op))
if not r:
self.clean()
raise Exception('can not initialize cipher context')
def encrypt(self, data):
return self.update(data)
def decrypt(self, data):
return self.update(data)
ciphers = {
'aes-128-cfb': (16, 16, OpenSSLCrypto),
'aes-192-cfb': (24, 16, OpenSSLCrypto),
'aes-256-cfb': (32, 16, OpenSSLCrypto),
'aes-128-ofb': (16, 16, OpenSSLCrypto),
'aes-192-ofb': (24, 16, OpenSSLCrypto),
'aes-256-ofb': (32, 16, OpenSSLCrypto),
'aes-128-ctr': (16, 16, OpenSSLCrypto),
'aes-192-ctr': (24, 16, OpenSSLCrypto),
'aes-256-ctr': (32, 16, OpenSSLCrypto),
'aes-128-cfb8': (16, 16, OpenSSLCrypto),
'aes-192-cfb8': (24, 16, OpenSSLCrypto),
'aes-256-cfb8': (32, 16, OpenSSLCrypto),
'aes-128-cfb1': (16, 16, OpenSSLCrypto),
'aes-192-cfb1': (24, 16, OpenSSLCrypto),
'aes-256-cfb1': (32, 16, OpenSSLCrypto),
'bf-cfb': (16, 8, OpenSSLCrypto),
'camellia-128-cfb': (16, 16, OpenSSLCrypto),
'camellia-192-cfb': (24, 16, OpenSSLCrypto),
'camellia-256-cfb': (32, 16, OpenSSLCrypto),
'cast5-cfb': (16, 8, OpenSSLCrypto),
'des-cfb': (8, 8, OpenSSLCrypto),
'idea-cfb': (16, 8, OpenSSLCrypto),
'rc2-cfb': (16, 8, OpenSSLCrypto),
'rc4': (16, 0, OpenSSLCrypto),
'seed-cfb': (16, 16, OpenSSLCrypto),
'aes-128-cfb': (16, 16, OpenSSLStreamCrypto),
'aes-192-cfb': (24, 16, OpenSSLStreamCrypto),
'aes-256-cfb': (32, 16, OpenSSLStreamCrypto),
'aes-128-ofb': (16, 16, OpenSSLStreamCrypto),
'aes-192-ofb': (24, 16, OpenSSLStreamCrypto),
'aes-256-ofb': (32, 16, OpenSSLStreamCrypto),
'aes-128-ctr': (16, 16, OpenSSLStreamCrypto),
'aes-192-ctr': (24, 16, OpenSSLStreamCrypto),
'aes-256-ctr': (32, 16, OpenSSLStreamCrypto),
'aes-128-cfb8': (16, 16, OpenSSLStreamCrypto),
'aes-192-cfb8': (24, 16, OpenSSLStreamCrypto),
'aes-256-cfb8': (32, 16, OpenSSLStreamCrypto),
'aes-128-cfb1': (16, 16, OpenSSLStreamCrypto),
'aes-192-cfb1': (24, 16, OpenSSLStreamCrypto),
'aes-256-cfb1': (32, 16, OpenSSLStreamCrypto),
'bf-cfb': (16, 8, OpenSSLStreamCrypto),
'camellia-128-cfb': (16, 16, OpenSSLStreamCrypto),
'camellia-192-cfb': (24, 16, OpenSSLStreamCrypto),
'camellia-256-cfb': (32, 16, OpenSSLStreamCrypto),
'cast5-cfb': (16, 8, OpenSSLStreamCrypto),
'des-cfb': (8, 8, OpenSSLStreamCrypto),
'idea-cfb': (16, 8, OpenSSLStreamCrypto),
'rc2-cfb': (16, 8, OpenSSLStreamCrypto),
'rc4': (16, 0, OpenSSLStreamCrypto),
'seed-cfb': (16, 16, OpenSSLStreamCrypto),
# AEAD: iv_len = salt_len = key_len
'aes-128-gcm': (16, 16, OpenSSLAeadCrypto),
'aes-192-gcm': (24, 24, OpenSSLAeadCrypto),
'aes-256-gcm': (32, 32, OpenSSLAeadCrypto),
'aes-128-ocb': (16, 16, OpenSSLAeadCrypto),
'aes-192-ocb': (24, 24, OpenSSLAeadCrypto),
'aes-256-ocb': (32, 32, OpenSSLAeadCrypto),
}
def run_method(method):
cipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 1)
decipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 0)
print(method, ': [stream]', 32)
cipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 1)
decipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher)
def run_aead_method(method, key_len=16):
if not loaded:
load_openssl(None)
print(method, ': [payload][tag]', key_len)
cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(method))
if not cipher:
cipher = load_cipher(common.to_bytes(method))
if not cipher:
print('cipher not avaiable, please upgrade openssl')
return
key_len = int(key_len)
cipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1)
decipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 0)
util.run_cipher(cipher, decipher)
def run_aead_method_chunk(method, key_len=16):
if not loaded:
load_openssl(None)
print(method, ': chunk([size][tag][payload][tag]', key_len)
cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(method))
if not cipher:
cipher = load_cipher(common.to_bytes(method))
if not cipher:
print('cipher not avaiable, please upgrade openssl')
return
key_len = int(key_len)
cipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1)
decipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 0)
cipher.encrypt_once = cipher.encrypt
decipher.decrypt_once = decipher.decrypt
util.run_cipher(cipher, decipher)
def test_aes_gcm(bits=128):
method = "aes-{0}-gcm".format(bits)
run_aead_method(method, bits / 8)
def test_aes_ocb(bits=128):
method = "aes-{0}-ocb".format(bits)
run_aead_method(method, bits / 8)
def test_aes_gcm_chunk(bits=128):
method = "aes-{0}-gcm".format(bits)
run_aead_method_chunk(method, bits / 8)
def test_aes_ocb_chunk(bits=128):
method = "aes-{0}-ocb".format(bits)
run_aead_method_chunk(method, bits / 8)
def test_aes_128_cfb():
run_method('aes-128-cfb')
@ -184,3 +432,17 @@ def test_rc4():
if __name__ == '__main__':
test_aes_128_cfb()
test_aes_256_cfb()
test_aes_256_ofb()
test_aes_gcm(128)
test_aes_gcm(192)
test_aes_gcm(256)
test_aes_gcm_chunk(128)
test_aes_gcm_chunk(192)
test_aes_gcm_chunk(256)
test_aes_ocb(128)
test_aes_ocb(192)
test_aes_ocb(256)
test_aes_ocb_chunk(128)
test_aes_ocb_chunk(192)
test_aes_ocb_chunk(256)

View file

@ -18,19 +18,19 @@ from __future__ import absolute_import, division, print_function, \
with_statement
import hashlib
from shadowsocks.crypto import openssl
__all__ = ['ciphers']
def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None,
def create_cipher(alg, key, iv, op, crypto_path=None,
key_as_bytes=0, d=None, salt=None,
i=1, padding=1):
md5 = hashlib.md5()
md5.update(key)
md5.update(iv)
rc4_key = md5.digest()
return openssl.OpenSSLCrypto(b'rc4', rc4_key, b'', op)
return openssl.OpenSSLStreamCrypto(b'rc4', rc4_key, b'', op, crypto_path)
ciphers = {

View file

@ -17,56 +17,162 @@
from __future__ import absolute_import, division, print_function, \
with_statement
from ctypes import c_char_p, c_int, c_ulonglong, byref, c_ulong, \
from ctypes import c_char_p, c_int, c_uint, c_ulonglong, byref, \
create_string_buffer, c_void_p
from shadowsocks.crypto import util
from shadowsocks.crypto import aead
from shadowsocks.crypto.aead import AeadCryptoBase
__all__ = ['ciphers']
libsodium = None
loaded = False
buf = None
buf_size = 2048
# for salsa20 and chacha20 and chacha20-ietf
BLOCK_SIZE = 64
def load_libsodium():
def load_libsodium(crypto_path=None):
global loaded, libsodium, buf
libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic',
'libsodium')
if libsodium is None:
raise Exception('libsodium not found')
crypto_path = dict(crypto_path) if crypto_path else dict()
path = crypto_path.get('sodium', None)
if not aead.sodium_loaded:
aead.load_sodium(path)
if aead.sodium_loaded:
libsodium = aead.libsodium
else:
print('load libsodium again with path %s' % path)
libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic',
'libsodium', path)
if libsodium is None:
raise Exception('libsodium not found')
if libsodium.sodium_init() < 0:
raise Exception('libsodium init failed')
libsodium.crypto_stream_salsa20_xor_ic.restype = c_int
libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p,
c_ulonglong,
c_char_p, c_ulonglong,
c_char_p)
libsodium.crypto_stream_salsa20_xor_ic.argtypes = (
c_void_p, c_char_p, # cipher output, msg
c_ulonglong, # msg len
c_char_p, c_ulonglong, # nonce, uint64_t initial block counter
c_char_p # key
)
libsodium.crypto_stream_chacha20_xor_ic.restype = c_int
libsodium.crypto_stream_chacha20_xor_ic.argtypes = (c_void_p, c_char_p,
c_ulonglong,
c_char_p, c_ulonglong,
c_char_p)
libsodium.crypto_stream_chacha20_xor_ic.argtypes = (
c_void_p, c_char_p,
c_ulonglong,
c_char_p, c_ulonglong,
c_char_p
)
if hasattr(libsodium, 'crypto_stream_xchacha20_xor_ic'):
libsodium.crypto_stream_xchacha20_xor_ic.restype = c_int
libsodium.crypto_stream_xchacha20_xor_ic.argtypes = (
c_void_p, c_char_p,
c_ulonglong,
c_char_p, c_ulonglong,
c_char_p
)
libsodium.crypto_stream_chacha20_ietf_xor_ic.restype = c_int
libsodium.crypto_stream_chacha20_ietf_xor_ic.argtypes = (c_void_p,
c_char_p,
c_ulonglong,
c_char_p,
c_ulong,
c_char_p)
libsodium.crypto_stream_chacha20_ietf_xor_ic.argtypes = (
c_void_p, c_char_p,
c_ulonglong,
c_char_p,
c_uint, # uint32_t initial counter
c_char_p
)
# chacha20-poly1305
libsodium.crypto_aead_chacha20poly1305_encrypt.restype = c_int
libsodium.crypto_aead_chacha20poly1305_encrypt.argtypes = (
c_void_p, c_void_p, # c, clen
c_char_p, c_ulonglong, # m, mlen
c_char_p, c_ulonglong, # ad, adlen
c_char_p, # nsec, not used
c_char_p, c_char_p # npub, k
)
libsodium.crypto_aead_chacha20poly1305_decrypt.restype = c_int
libsodium.crypto_aead_chacha20poly1305_decrypt.argtypes = (
c_void_p, c_void_p, # m, mlen
c_char_p, # nsec, not used
c_char_p, c_ulonglong, # c, clen
c_char_p, c_ulonglong, # ad, adlen
c_char_p, c_char_p # npub, k
)
# chacha20-ietf-poly1305, same api structure as above
libsodium.crypto_aead_chacha20poly1305_ietf_encrypt.restype = c_int
libsodium.crypto_aead_chacha20poly1305_ietf_encrypt.argtypes = (
c_void_p, c_void_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p,
c_char_p, c_char_p
)
libsodium.crypto_aead_chacha20poly1305_ietf_decrypt.restype = c_int
libsodium.crypto_aead_chacha20poly1305_ietf_decrypt.argtypes = (
c_void_p, c_void_p,
c_char_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p, c_char_p
)
# xchacha20-ietf-poly1305, same api structure as above
if hasattr(libsodium, 'crypto_aead_xchacha20poly1305_ietf_encrypt'):
libsodium.crypto_aead_xchacha20poly1305_ietf_encrypt.restype = c_int
libsodium.crypto_aead_xchacha20poly1305_ietf_encrypt.argtypes = (
c_void_p, c_void_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p,
c_char_p, c_char_p
)
libsodium.crypto_aead_xchacha20poly1305_ietf_decrypt.restype = c_int
libsodium.crypto_aead_xchacha20poly1305_ietf_decrypt.argtypes = (
c_void_p, c_void_p,
c_char_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p, c_char_p
)
# aes-256-gcm, same api structure as above
libsodium.crypto_aead_aes256gcm_is_available.restype = c_int
if libsodium.crypto_aead_aes256gcm_is_available():
libsodium.crypto_aead_aes256gcm_encrypt.restype = c_int
libsodium.crypto_aead_aes256gcm_encrypt.argtypes = (
c_void_p, c_void_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p,
c_char_p, c_char_p
)
libsodium.crypto_aead_aes256gcm_decrypt.restype = c_int
libsodium.crypto_aead_aes256gcm_decrypt.argtypes = (
c_void_p, c_void_p,
c_char_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p, c_char_p
)
buf = create_string_buffer(buf_size)
loaded = True
class SodiumCrypto(object):
def __init__(self, cipher_name, key, iv, op):
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
if not loaded:
load_libsodium()
load_libsodium(crypto_path)
self.key = key
self.iv = iv
self.key_ptr = c_char_p(key)
@ -75,6 +181,11 @@ class SodiumCrypto(object):
self.cipher = libsodium.crypto_stream_salsa20_xor_ic
elif cipher_name == 'chacha20':
self.cipher = libsodium.crypto_stream_chacha20_xor_ic
elif cipher_name == 'xchacha20':
if hasattr(libsodium, 'crypto_stream_xchacha20_xor_ic'):
self.cipher = libsodium.crypto_stream_xchacha20_xor_ic
else:
raise Exception('Unsupported cipher')
elif cipher_name == 'chacha20-ietf':
self.cipher = libsodium.crypto_stream_chacha20_ietf_xor_ic
else:
@ -82,6 +193,18 @@ class SodiumCrypto(object):
# byte counter, not block counter
self.counter = 0
def encrypt(self, data):
return self.update(data)
def decrypt(self, data):
return self.update(data)
def encrypt_once(self, data):
return self.update(data)
def decrypt_once(self, data):
return self.update(data)
def update(self, data):
global buf_size, buf
l = len(data)
@ -102,38 +225,218 @@ class SodiumCrypto(object):
# strip off the padding
return buf.raw[padding:padding + l]
def clean(self):
pass
class SodiumAeadCrypto(AeadCryptoBase):
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
if not loaded:
load_libsodium(crypto_path)
AeadCryptoBase.__init__(self, cipher_name, key, iv, op, crypto_path)
if cipher_name == 'chacha20-poly1305':
self.encryptor = libsodium.crypto_aead_chacha20poly1305_encrypt
self.decryptor = libsodium.crypto_aead_chacha20poly1305_decrypt
elif cipher_name == 'chacha20-ietf-poly1305':
self.encryptor = libsodium. \
crypto_aead_chacha20poly1305_ietf_encrypt
self.decryptor = libsodium. \
crypto_aead_chacha20poly1305_ietf_decrypt
elif cipher_name == 'xchacha20-ietf-poly1305':
if hasattr(libsodium,
'crypto_aead_xchacha20poly1305_ietf_encrypt'):
self.encryptor = libsodium. \
crypto_aead_xchacha20poly1305_ietf_encrypt
self.decryptor = libsodium. \
crypto_aead_xchacha20poly1305_ietf_decrypt
else:
raise Exception('Unsupported cipher')
elif cipher_name == 'sodium:aes-256-gcm':
if hasattr(libsodium, 'crypto_aead_aes256gcm_encrypt'):
self.encryptor = libsodium.crypto_aead_aes256gcm_encrypt
self.decryptor = libsodium.crypto_aead_aes256gcm_decrypt
else:
raise Exception('Unsupported cipher')
else:
raise Exception('Unknown cipher')
def cipher_ctx_init(self):
global libsodium
libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen))
# print("".join("%02x" % ord(b) for b in self._nonce))
def aead_encrypt(self, data):
global buf, buf_size
plen = len(data)
if buf_size < plen + self._tlen:
buf_size = (plen + self._tlen) * 2
buf = create_string_buffer(buf_size)
cipher_out_len = c_ulonglong(0)
self.encryptor(
byref(buf), byref(cipher_out_len),
c_char_p(data), c_ulonglong(plen),
None, c_ulonglong(0), None,
c_char_p(self._nonce.raw), c_char_p(self._skey)
)
if cipher_out_len.value != plen + self._tlen:
raise Exception("Encrypt failed")
self.cipher_ctx_init()
return buf.raw[:cipher_out_len.value]
def aead_decrypt(self, data):
global buf, buf_size
clen = len(data)
if buf_size < clen:
buf_size = clen * 2
buf = create_string_buffer(buf_size)
cipher_out_len = c_ulonglong(0)
r = self.decryptor(
byref(buf), byref(cipher_out_len),
None,
c_char_p(data), c_ulonglong(clen),
None, c_ulonglong(0),
c_char_p(self._nonce.raw), c_char_p(self._skey)
)
if r != 0:
raise Exception("Decrypt failed")
if cipher_out_len.value != clen - self._tlen:
raise Exception("Decrypt failed")
self.cipher_ctx_init()
return buf.raw[:cipher_out_len.value]
def encrypt_once(self, data):
return self.aead_encrypt(data)
def decrypt_once(self, data):
return self.aead_decrypt(data)
ciphers = {
'salsa20': (32, 8, SodiumCrypto),
'chacha20': (32, 8, SodiumCrypto),
'xchacha20': (32, 24, SodiumCrypto),
'chacha20-ietf': (32, 12, SodiumCrypto),
# AEAD: iv_len = salt_len = key_len
'chacha20-poly1305': (32, 32, SodiumAeadCrypto),
'chacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto),
'xchacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto),
'sodium:aes-256-gcm': (32, 32, SodiumAeadCrypto),
}
def test_salsa20():
cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1)
decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher)
def test_chacha20():
print("Test chacha20")
cipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 1)
decipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher)
def test_chacha20_ietf():
def test_xchacha20():
print("Test xchacha20")
cipher = SodiumCrypto('xchacha20', b'k' * 32, b'i' * 24, 1)
decipher = SodiumCrypto('xchacha20', b'k' * 32, b'i' * 24, 0)
util.run_cipher(cipher, decipher)
def test_salsa20():
print("Test salsa20")
cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1)
decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher)
def test_chacha20_ietf():
print("Test chacha20-ietf")
cipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 1)
decipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher)
def test_chacha20_poly1305():
print("Test chacha20-poly1305 [payload][tag]")
cipher = SodiumAeadCrypto('chacha20-poly1305',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('chacha20-poly1305',
b'k' * 32, b'i' * 32, 0)
util.run_cipher(cipher, decipher)
def test_chacha20_poly1305_chunk():
print("Test chacha20-poly1305 chunk [size][tag][payload][tag]")
cipher = SodiumAeadCrypto('chacha20-poly1305',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('chacha20-poly1305',
b'k' * 32, b'i' * 32, 0)
cipher.encrypt_once = cipher.encrypt
decipher.decrypt_once = decipher.decrypt
util.run_cipher(cipher, decipher)
def test_chacha20_ietf_poly1305():
print("Test chacha20-ietf-poly1305 [payload][tag]")
cipher = SodiumAeadCrypto('chacha20-ietf-poly1305',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('chacha20-ietf-poly1305',
b'k' * 32, b'i' * 32, 0)
util.run_cipher(cipher, decipher)
def test_chacha20_ietf_poly1305_chunk():
print("Test chacha20-ietf-poly1305 chunk [size][tag][payload][tag]")
cipher = SodiumAeadCrypto('chacha20-ietf-poly1305',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('chacha20-ietf-poly1305',
b'k' * 32, b'i' * 32, 0)
cipher.encrypt_once = cipher.encrypt
decipher.decrypt_once = decipher.decrypt
util.run_cipher(cipher, decipher)
def test_aes_256_gcm():
print("Test sodium:aes-256-gcm [payload][tag]")
cipher = SodiumAeadCrypto('sodium:aes-256-gcm',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('sodium:aes-256-gcm',
b'k' * 32, b'i' * 32, 0)
util.run_cipher(cipher, decipher)
def test_aes_256_gcm_chunk():
print("Test sodium:aes-256-gcm chunk [size][tag][payload][tag]")
cipher = SodiumAeadCrypto('sodium:aes-256-gcm',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('sodium:aes-256-gcm',
b'k' * 32, b'i' * 32, 0)
cipher.encrypt_once = cipher.encrypt
decipher.decrypt_once = decipher.decrypt
util.run_cipher(cipher, decipher)
if __name__ == '__main__':
test_chacha20()
test_xchacha20()
test_salsa20()
test_chacha20_ietf()
test_chacha20_poly1305()
test_chacha20_poly1305_chunk()
test_chacha20_ietf_poly1305()
test_chacha20_ietf_poly1305_chunk()
test_aes_256_gcm()
test_aes_256_gcm_chunk()

View file

@ -55,9 +55,13 @@ def init_table(key):
class TableCipher(object):
def __init__(self, cipher_name, key, iv, op):
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
self._encrypt_table, self._decrypt_table = init_table(key)
self._op = op
self.encrypt = self.update
self.decrypt = self.update
self.encrypt_once = self.update
self.decrypt_once = self.update
def update(self, data):
if self._op:

View file

@ -26,6 +26,7 @@ def find_library_nt(name):
# ctypes.util.find_library just returns first result he found
# but we want to try them all
# because on Windows, users may have both 32bit and 64bit version installed
import glob
results = []
for directory in os.environ['PATH'].split(os.pathsep):
fname = os.path.join(directory, name)
@ -33,15 +34,34 @@ def find_library_nt(name):
results.append(fname)
if fname.lower().endswith(".dll"):
continue
fname = fname + ".dll"
if os.path.isfile(fname):
results.append(fname)
fname += "*.dll"
files = glob.glob(fname)
if files:
results.extend(files)
return results
def find_library(possible_lib_names, search_symbol, library_name):
import ctypes.util
def load_library(path, search_symbol, library_name):
from ctypes import CDLL
try:
lib = CDLL(path)
if hasattr(lib, search_symbol):
logging.info('loading %s from %s', library_name, path)
return lib
else:
logging.warn('can\'t find symbol %s in %s', search_symbol,
path)
except Exception:
pass
return None
def find_library(possible_lib_names, search_symbol, library_name,
custom_path=None):
import ctypes.util
if custom_path:
return load_library(custom_path, search_symbol, library_name)
paths = []
@ -79,16 +99,22 @@ def find_library(possible_lib_names, search_symbol, library_name):
if files:
paths.extend(files)
for path in paths:
try:
lib = CDLL(path)
if hasattr(lib, search_symbol):
logging.info('loading %s from %s', library_name, path)
return lib
else:
logging.warn('can\'t find symbol %s in %s', search_symbol,
path)
except Exception:
pass
lib = load_library(path, search_symbol, library_name)
if lib:
return lib
return None
def parse_mode(cipher_nme):
"""
Parse the cipher mode from cipher name
e.g. aes-128-gcm, the mode is gcm
:param cipher_nme: str cipher name, aes-128-cfb, aes-128-gcm ...
:return: str/None The mode, cfb, gcm ...
"""
hyphen = cipher_nme.rfind('-')
if hyphen > 0:
return cipher_nme[hyphen:]
return None
@ -97,29 +123,31 @@ def run_cipher(cipher, decipher):
import random
import time
BLOCK_SIZE = 16384
block_size = 16384
rounds = 1 * 1024
plain = urandom(BLOCK_SIZE * rounds)
plain = urandom(block_size * rounds)
results = []
cipher_results = []
pos = 0
print('test start')
start = time.time()
while pos < len(plain):
l = random.randint(100, 32768)
c = cipher.update(plain[pos:pos + l])
results.append(c)
# print(pos, l)
c = cipher.encrypt_once(plain[pos:pos + l])
cipher_results.append(c)
pos += l
pos = 0
c = b''.join(results)
results = []
while pos < len(plain):
l = random.randint(100, 32768)
results.append(decipher.update(c[pos:pos + l]))
# c = b''.join(cipher_results)
plain_results = []
for c in cipher_results:
# l = random.randint(100, 32768)
l = len(c)
plain_results.append(decipher.decrypt_once(c))
pos += l
end = time.time()
print('speed: %d bytes/s' % (BLOCK_SIZE * rounds / (end - start)))
assert b''.join(results) == plain
print('speed: %d bytes/s' % (block_size * rounds / (end - start)))
assert b''.join(plain_results) == plain
def test_find_library():

View file

@ -23,12 +23,20 @@ import hashlib
import logging
from shadowsocks import common
from shadowsocks.crypto import rc4_md5, openssl, sodium, table
from shadowsocks.crypto import rc4_md5, openssl, mbedtls, sodium, table
CIPHER_ENC_ENCRYPTION = 1
CIPHER_ENC_DECRYPTION = 0
METHOD_INFO_KEY_LEN = 0
METHOD_INFO_IV_LEN = 1
METHOD_INFO_CRYPTO = 2
method_supported = {}
method_supported.update(rc4_md5.ciphers)
method_supported.update(openssl.ciphers)
method_supported.update(mbedtls.ciphers)
method_supported.update(sodium.ciphers)
method_supported.update(table.ciphers)
@ -36,12 +44,11 @@ method_supported.update(table.ciphers)
def random_string(length):
return os.urandom(length)
cached_keys = {}
def try_cipher(key, method=None):
Encryptor(key, method)
def try_cipher(key, method=None, crypto_path=None):
Cryptor(key, method, crypto_path)
def EVP_BytesToKey(password, key_len, iv_len):
@ -68,8 +75,15 @@ def EVP_BytesToKey(password, key_len, iv_len):
return key, iv
class Encryptor(object):
def __init__(self, password, method):
class Cryptor(object):
def __init__(self, password, method, crypto_path=None):
"""
Crypto wrapper
:param password: str cipher password
:param method: str cipher
:param crypto_path: dict or none
{'openssl': path, 'sodium': path, 'mbedtls': path}
"""
self.password = password
self.key = None
self.method = method
@ -77,16 +91,20 @@ class Encryptor(object):
self.cipher_iv = b''
self.decipher = None
self.decipher_iv = None
self.crypto_path = crypto_path
method = method.lower()
self._method_info = self.get_method_info(method)
self._method_info = Cryptor.get_method_info(method)
if self._method_info:
self.cipher = self.get_cipher(password, method, 1,
random_string(self._method_info[1]))
self.cipher = self.get_cipher(
password, method, CIPHER_ENC_ENCRYPTION,
random_string(self._method_info[METHOD_INFO_IV_LEN])
)
else:
logging.error('method %s not supported' % method)
sys.exit(1)
def get_method_info(self, method):
@staticmethod
def get_method_info(method):
method = method.lower()
m = method_supported.get(method)
return m
@ -97,46 +115,50 @@ class Encryptor(object):
def get_cipher(self, password, method, op, iv):
password = common.to_bytes(password)
m = self._method_info
if m[0] > 0:
key, iv_ = EVP_BytesToKey(password, m[0], m[1])
if m[METHOD_INFO_KEY_LEN] > 0:
key, _ = EVP_BytesToKey(password,
m[METHOD_INFO_KEY_LEN],
m[METHOD_INFO_IV_LEN])
else:
# key_length == 0 indicates we should use the key directly
key, iv = password, b''
self.key = key
iv = iv[:m[1]]
if op == 1:
iv = iv[:m[METHOD_INFO_IV_LEN]]
if op == CIPHER_ENC_ENCRYPTION:
# this iv is for cipher not decipher
self.cipher_iv = iv[:m[1]]
return m[2](method, key, iv, op)
self.cipher_iv = iv
return m[METHOD_INFO_CRYPTO](method, key, iv, op, self.crypto_path)
def encrypt(self, buf):
if len(buf) == 0:
return buf
if self.iv_sent:
return self.cipher.update(buf)
return self.cipher.encrypt(buf)
else:
self.iv_sent = True
return self.cipher_iv + self.cipher.update(buf)
return self.cipher_iv + self.cipher.encrypt(buf)
def decrypt(self, buf):
if len(buf) == 0:
return buf
if self.decipher is None:
decipher_iv_len = self._method_info[1]
decipher_iv_len = self._method_info[METHOD_INFO_IV_LEN]
decipher_iv = buf[:decipher_iv_len]
self.decipher_iv = decipher_iv
self.decipher = self.get_cipher(self.password, self.method, 0,
iv=decipher_iv)
self.decipher = self.get_cipher(
self.password, self.method,
CIPHER_ENC_DECRYPTION,
decipher_iv
)
buf = buf[decipher_iv_len:]
if len(buf) == 0:
return buf
return self.decipher.update(buf)
return self.decipher.decrypt(buf)
def gen_key_iv(password, method):
method = method.lower()
(key_len, iv_len, m) = method_supported[method]
key = None
if key_len > 0:
key, _ = EVP_BytesToKey(password, key_len, iv_len)
else:
@ -145,53 +167,38 @@ def gen_key_iv(password, method):
return key, iv, m
def encrypt_all_m(key, iv, m, method, data):
result = []
result.append(iv)
cipher = m(method, key, iv, 1)
result.append(cipher.update(data))
def encrypt_all_m(key, iv, m, method, data, crypto_path=None):
result = [iv]
cipher = m(method, key, iv, 1, crypto_path)
result.append(cipher.encrypt_once(data))
return b''.join(result)
def dencrypt_all(password, method, data):
def decrypt_all(password, method, data, crypto_path=None):
result = []
method = method.lower()
(key_len, iv_len, m) = method_supported[method]
key = None
if key_len > 0:
key, _ = EVP_BytesToKey(password, key_len, iv_len)
else:
key = password
iv = data[:iv_len]
data = data[iv_len:]
cipher = m(method, key, iv, 0)
result.append(cipher.update(data))
(key, iv, m) = gen_key_iv(password, method)
iv = data[:len(iv)]
data = data[len(iv):]
cipher = m(method, key, iv, CIPHER_ENC_DECRYPTION, crypto_path)
result.append(cipher.decrypt_once(data))
return b''.join(result), key, iv
def encrypt_all(password, method, op, data):
def encrypt_all(password, method, data, crypto_path=None):
result = []
method = method.lower()
(key_len, iv_len, m) = method_supported[method]
key = None
if key_len > 0:
key, _ = EVP_BytesToKey(password, key_len, iv_len)
else:
key = password
if op:
iv = random_string(iv_len)
result.append(iv)
else:
iv = data[:iv_len]
data = data[iv_len:]
cipher = m(method, key, iv, op)
result.append(cipher.update(data))
(key, iv, m) = gen_key_iv(password, method)
result.append(iv)
cipher = m(method, key, iv, CIPHER_ENC_ENCRYPTION, crypto_path)
result.append(cipher.encrypt_once(data))
return b''.join(result)
CIPHERS_TO_TEST = [
'aes-128-cfb',
'aes-256-cfb',
'aes-256-gcm',
'rc4-md5',
'salsa20',
'chacha20',
@ -204,8 +211,8 @@ def test_encryptor():
plain = urandom(10240)
for method in CIPHERS_TO_TEST:
logging.warn(method)
encryptor = Encryptor(b'key', method)
decryptor = Encryptor(b'key', method)
encryptor = Cryptor(b'key', method)
decryptor = Cryptor(b'key', method)
cipher = encryptor.encrypt(plain)
plain2 = decryptor.decrypt(cipher)
assert plain == plain2
@ -216,8 +223,8 @@ def test_encrypt_all():
plain = urandom(10240)
for method in CIPHERS_TO_TEST:
logging.warn(method)
cipher = encrypt_all(b'key', method, 1, plain)
plain2 = encrypt_all(b'key', method, 0, cipher)
cipher = encrypt_all(b'key', method, plain)
plain2, key, iv = decrypt_all(b'key', method, cipher)
assert plain == plain2
@ -228,7 +235,7 @@ def test_encrypt_all_m():
logging.warn(method)
key, iv, m = gen_key_iv(b'key', method)
cipher = encrypt_all_m(key, iv, m, method, plain)
plain2, key, iv = dencrypt_all(b'key', method, cipher)
plain2, key, iv = decrypt_all(b'key', method, cipher)
assert plain == plain2

View file

@ -79,18 +79,15 @@ class LRUCache(collections.MutableMapping):
least = self._last_visits[0]
if now - least <= self.timeout:
break
if self.close_callback is not None:
for key in self._time_to_keys[least]:
if key in self._store:
if now - self._keys_to_last_time[key] > self.timeout:
value = self._store[key]
if value not in self._closed_values:
self.close_callback(value)
self._closed_values.add(value)
self._last_visits.popleft()
for key in self._time_to_keys[least]:
if key in self._store:
if now - self._keys_to_last_time[key] > self.timeout:
if self.close_callback is not None:
value = self._store[key]
if value not in self._closed_values:
self.close_callback(value)
self._closed_values.add(value)
del self._store[key]
del self._keys_to_last_time[key]
c += 1
@ -140,6 +137,7 @@ def test():
c = LRUCache(timeout=0.1, close_callback=close_cb)
c['s'] = 1
c['t'] = 1
c['s']
time.sleep(0.1)
c['s']

View file

@ -71,6 +71,7 @@ class Manager(object):
port_password = config['port_password']
del config['port_password']
config['crypto_path'] = config.get('crypto_path', dict())
for port, password in port_password.items():
a_config = config.copy()
a_config['server_port'] = int(port)
@ -202,7 +203,7 @@ def test():
import time
import threading
import struct
from shadowsocks import encrypt
from shadowsocks import cryptor
logging.basicConfig(level=5,
format='%(asctime)s %(levelname)-8s %(message)s',
@ -252,7 +253,7 @@ def test():
# test statistics for TCP
header = common.pack_addr(b'google.com') + struct.pack('>H', 80)
data = encrypt.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', 1,
data = cryptor.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb',
header + b'GET /\r\n\r\n')
tcp_cli = socket.socket()
tcp_cli.connect(('127.0.0.1', 7001))
@ -270,7 +271,7 @@ def test():
# test statistics for UDP
header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80)
data = encrypt.encrypt_all(b'foobar2', 'aes-256-cfb', 1,
data = cryptor.encrypt_all(b'foobar2', 'aes-256-cfb',
header + b'test')
udp_cli = socket.socket(type=socket.SOCK_DGRAM)
udp_cli.sendto(data, ('127.0.0.1', 8382))

View file

@ -28,7 +28,7 @@ import traceback
from functools import wraps
from shadowsocks.common import to_bytes, to_str, IPNetwork
from shadowsocks import encrypt
from shadowsocks import cryptor
VERBOSE_LEVEL = 5
@ -132,6 +132,13 @@ def check_config(config, is_local):
sys.exit(2)
else:
config['server'] = to_str(config['server'])
if config.get('tunnel_remote', None) is None:
logging.error('tunnel_remote addr not specified')
print_local_help()
sys.exit(2)
else:
config['tunnel_remote'] = to_str(config['tunnel_remote'])
else:
config['server'] = to_str(config.get('server', '0.0.0.0'))
try:
@ -159,6 +166,11 @@ def check_config(config, is_local):
if 'server_port' in config and type(config['server_port']) != list:
config['server_port'] = int(config['server_port'])
if 'tunnel_remote_port' in config:
config['tunnel_remote_port'] = int(config['tunnel_remote_port'])
if 'tunnel_port' in config:
config['tunnel_port'] = int(config['tunnel_port'])
if config.get('local_address', '') in [b'0.0.0.0']:
logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe')
if config.get('server', '') in ['127.0.0.1', 'localhost']:
@ -184,8 +196,19 @@ def check_config(config, is_local):
if os.name != 'posix':
logging.error('user can be used only on Unix')
sys.exit(1)
if config.get('dns_server', None) is not None:
if type(config['dns_server']) != list:
config['dns_server'] = to_str(config['dns_server'])
else:
config['dns_server'] = [to_str(ds) for ds in config['dns_server']]
logging.info('Specified DNS server: %s' % config['dns_server'])
encrypt.try_cipher(config['password'], config['method'])
config['crypto_path'] = {'openssl': config['libopenssl'],
'mbedtls': config['libmbedtls'],
'sodium': config['libsodium']}
cryptor.try_cipher(config['password'], config['method'],
config['crypto_path'])
def get_config(is_local):
@ -196,12 +219,12 @@ def get_config(is_local):
if is_local:
shortopts = 'hd:s:b:p:k:l:m:c:t:vqa'
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=',
'version']
'libopenssl=', 'libmbedtls=', 'libsodium=', 'version']
else:
shortopts = 'hd:s:p:k:m:c:t:vqa'
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=',
'forbidden-ip=', 'user=', 'manager-address=', 'version',
'prefer-ipv6']
'libopenssl=', 'libmbedtls=', 'libsodium=', 'prefer-ipv6']
try:
config_path = find_config()
optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
@ -245,10 +268,16 @@ def get_config(is_local):
config['timeout'] = int(value)
elif key == '--fast-open':
config['fast_open'] = True
elif key == '--libopenssl':
config['libopenssl'] = to_str(value)
elif key == '--libmbedtls':
config['libmbedtls'] = to_str(value)
elif key == '--libsodium':
config['libsodium'] = to_str(value)
elif key == '--workers':
config['workers'] = int(value)
elif key == '--manager-address':
config['manager_address'] = value
config['manager_address'] = to_str(value)
elif key == '--user':
config['user'] = to_str(value)
elif key == '--forbidden-ip':
@ -297,6 +326,14 @@ def get_config(is_local):
config['one_time_auth'] = config.get('one_time_auth', False)
config['prefer_ipv6'] = config.get('prefer_ipv6', False)
config['server_port'] = config.get('server_port', 8388)
config['dns_server'] = config.get('dns_server', None)
config['libopenssl'] = config.get('libopenssl', None)
config['libmbedtls'] = config.get('libmbedtls', None)
config['libsodium'] = config.get('libsodium', None)
config['tunnel_remote'] = to_str(config.get('tunnel_remote', '8.8.8.8'))
config['tunnel_remote_port'] = config.get('tunnel_remote_port', 53)
config['tunnel_port'] = config.get('tunnel_port', 53)
logging.getLogger('').handlers = []
logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE')
@ -341,16 +378,40 @@ Proxy options:
-l LOCAL_PORT local port, default: 1080
-k PASSWORD password
-m METHOD encryption method, default: aes-256-cfb
Sodium:
chacha20-poly1305, chacha20-ietf-poly1305,
xchacha20-ietf-poly1305,
sodium:aes-256-gcm,
salsa20, chacha20, chacha20-ietf.
Sodium 1.0.12:
xchacha20
OpenSSL:
aes-{128|192|256}-gcm, aes-{128|192|256}-cfb,
aes-{128|192|256}-ofb, aes-{128|192|256}-ctr,
camellia-{128|192|256}-cfb,
bf-cfb, cast5-cfb, des-cfb, idea-cfb,
rc2-cfb, seed-cfb,
rc4, rc4-md5, table.
OpenSSL 1.1:
aes-{128|192|256}-ocb
mbedTLS:
mbedtls:aes-{128|192|256}-cfb128,
mbedtls:aes-{128|192|256}-ctr,
mbedtls:camellia-{128|192|256}-cfb128,
mbedtls:aes-{128|192|256}-gcm
-t TIMEOUT timeout in seconds, default: 300
-a ONE_TIME_AUTH one time auth
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
--libopenssl=PATH custom openssl crypto lib path
--libmbedtls=PATH custom mbedtls crypto lib path
--libsodium=PATH custom sodium crypto lib path
General options:
-h, --help show this help message and exit
-d start/stop/restart daemon mode
--pid-file PID_FILE pid file for daemon mode
--log-file LOG_FILE log file for daemon mode
--user USER username to run as
--pid-file=PID_FILE pid file for daemon mode
--log-file=LOG_FILE log file for daemon mode
--user=USER username to run as
-v, -vv verbose mode
-q, -qq quiet mode, only show warnings/errors
--version show version information
@ -371,13 +432,37 @@ Proxy options:
-p SERVER_PORT server port, default: 8388
-k PASSWORD password
-m METHOD encryption method, default: aes-256-cfb
Sodium:
chacha20-poly1305, chacha20-ietf-poly1305,
xchacha20-ietf-poly1305,
sodium:aes-256-gcm,
salsa20, chacha20, chacha20-ietf.
Sodium 1.0.12:
xchacha20
OpenSSL:
aes-{128|192|256}-gcm, aes-{128|192|256}-cfb,
aes-{128|192|256}-ofb, aes-{128|192|256}-ctr,
camellia-{128|192|256}-cfb,
bf-cfb, cast5-cfb, des-cfb, idea-cfb,
rc2-cfb, seed-cfb,
rc4, rc4-md5, table.
OpenSSL 1.1:
aes-{128|192|256}-ocb
mbedTLS:
mbedtls:aes-{128|192|256}-cfb128,
mbedtls:aes-{128|192|256}-ctr,
mbedtls:camellia-{128|192|256}-cfb128,
mbedtls:aes-{128|192|256}-gcm
-t TIMEOUT timeout in seconds, default: 300
-a ONE_TIME_AUTH one time auth
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
--workers WORKERS number of workers, available on Unix/Linux
--forbidden-ip IPLIST comma seperated IP list forbidden to connect
--manager-address ADDR optional server manager UDP address, see wiki
--workers=WORKERS number of workers, available on Unix/Linux
--forbidden-ip=IPLIST comma seperated IP list forbidden to connect
--manager-address=ADDR optional server manager UDP address, see wiki
--prefer-ipv6 resolve ipv6 address first
--libopenssl=PATH custom openssl crypto lib path
--libmbedtls=PATH custom mbedtls crypto lib path
--libsodium=PATH custom sodium crypto lib path
General options:
-h, --help show this help message and exit

View file

@ -26,7 +26,7 @@ import logging
import traceback
import random
from shadowsocks import encrypt, eventloop, shell, common
from shadowsocks import cryptor, eventloop, shell, common
from shadowsocks.common import parse_header, onetimeauth_verify, \
onetimeauth_gen, ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, \
ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH
@ -93,10 +93,12 @@ WAIT_STATUS_WRITING = 2
WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING
BUF_SIZE = 32 * 1024
UP_STREAM_BUF_SIZE = 16 * 1024
DOWN_STREAM_BUF_SIZE = 32 * 1024
# helper exceptions for TCPRelayHandler
class BadSocksHeader(Exception):
pass
@ -106,6 +108,7 @@ class NoAcceptableMethods(Exception):
class TCPRelayHandler(object):
def __init__(self, server, fd_to_handlers, loop, local_sock, config,
dns_resolver, is_local):
self._server = server
@ -115,13 +118,18 @@ class TCPRelayHandler(object):
self._remote_sock = None
self._config = config
self._dns_resolver = dns_resolver
self.tunnel_remote = config.get('tunnel_remote', "8.8.8.8")
self.tunnel_remote_port = config.get('tunnel_remote_port', 53)
self.tunnel_port = config.get('tunnel_port', 53)
self._is_tunnel = server._is_tunnel
# TCP Relay works as either sslocal or ssserver
# if is_local, this is sslocal
self._is_local = is_local
self._stage = STAGE_INIT
self._encryptor = encrypt.Encryptor(config['password'],
config['method'])
self._cryptor = cryptor.Cryptor(config['password'],
config['method'],
config['crypto_path'])
self._ota_enable = config.get('one_time_auth', False)
self._ota_enable_session = self._ota_enable
self._ota_buff_head = b''
@ -250,10 +258,9 @@ class TCPRelayHandler(object):
else:
self._data_to_write_to_remote.append(data)
return
if self._ota_enable_session:
data = self._ota_chunk_data_gen(data)
data = self._encryptor.encrypt(data)
data = self._cryptor.encrypt(data)
self._data_to_write_to_remote.append(data)
if self._config['fast_open'] and not self._fastopen_connected:
@ -293,29 +300,36 @@ class TCPRelayHandler(object):
@shell.exception_handle(self_=True, destroy=True, conn_err=True)
def _handle_stage_addr(self, data):
if self._is_local:
cmd = common.ord(data[1])
if cmd == CMD_UDP_ASSOCIATE:
logging.debug('UDP associate')
if self._local_sock.family == socket.AF_INET6:
header = b'\x05\x00\x00\x04'
else:
header = b'\x05\x00\x00\x01'
addr, port = self._local_sock.getsockname()[:2]
addr_to_send = socket.inet_pton(self._local_sock.family,
addr)
port_to_send = struct.pack('>H', port)
self._write_to_sock(header + addr_to_send + port_to_send,
self._local_sock)
self._stage = STAGE_UDP_ASSOC
# just wait for the client to disconnect
return
elif cmd == CMD_CONNECT:
# just trim VER CMD RSV
data = data[3:]
if self._is_tunnel:
# add ss header to data
tunnel_remote = self.tunnel_remote
tunnel_remote_port = self.tunnel_remote_port
data = common.add_header(tunnel_remote,
tunnel_remote_port, data)
else:
logging.error('unknown command %d', cmd)
self.destroy()
return
cmd = common.ord(data[1])
if cmd == CMD_UDP_ASSOCIATE:
logging.debug('UDP associate')
if self._local_sock.family == socket.AF_INET6:
header = b'\x05\x00\x00\x04'
else:
header = b'\x05\x00\x00\x01'
addr, port = self._local_sock.getsockname()[:2]
addr_to_send = socket.inet_pton(self._local_sock.family,
addr)
port_to_send = struct.pack('>H', port)
self._write_to_sock(header + addr_to_send + port_to_send,
self._local_sock)
self._stage = STAGE_UDP_ASSOC
# just wait for the client to disconnect
return
elif cmd == CMD_CONNECT:
# just trim VER CMD RSV
data = data[3:]
else:
logging.error('unknown command %d', cmd)
self.destroy()
return
header_result = parse_header(data)
if header_result is None:
raise Exception('can not parse header')
@ -336,7 +350,7 @@ class TCPRelayHandler(object):
offset = header_length + ONETIMEAUTH_BYTES
_hash = data[header_length: offset]
_data = data[:header_length]
key = self._encryptor.decipher_iv + self._encryptor.key
key = self._cryptor.decipher_iv + self._cryptor.key
if onetimeauth_verify(_hash, _data, key) is False:
logging.warn('one time auth fail')
self.destroy()
@ -347,19 +361,21 @@ class TCPRelayHandler(object):
self._update_stream(STREAM_UP, WAIT_STATUS_WRITING)
self._stage = STAGE_DNS
if self._is_local:
# forward address to remote
self._write_to_sock((b'\x05\x00\x00\x01'
b'\x00\x00\x00\x00\x10\x10'),
self._local_sock)
# jump over socks5 response
if not self._is_tunnel:
# forward address to remote
self._write_to_sock((b'\x05\x00\x00\x01'
b'\x00\x00\x00\x00\x10\x10'),
self._local_sock)
# spec https://shadowsocks.org/en/spec/one-time-auth.html
# ATYP & 0x10 == 0x10, then OTA is enabled.
if self._ota_enable_session:
data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:]
key = self._encryptor.cipher_iv + self._encryptor.key
key = self._cryptor.cipher_iv + self._cryptor.key
_header = data[:header_length]
sha110 = onetimeauth_gen(data, key)
data = _header + sha110 + data[header_length:]
data_to_send = self._encryptor.encrypt(data)
data_to_send = self._cryptor.encrypt(data)
self._data_to_write_to_remote.append(data_to_send)
# notice here may go into _handle_dns_resolved directly
self._dns_resolver.resolve(self._chosen_server[0],
@ -462,7 +478,7 @@ class TCPRelayHandler(object):
_hash = self._ota_buff_head[ONETIMEAUTH_CHUNK_DATA_LEN:]
_data = self._ota_buff_data
index = struct.pack('>I', self._ota_chunk_idx)
key = self._encryptor.decipher_iv + index
key = self._cryptor.decipher_iv + index
if onetimeauth_verify(_hash, _data, key) is False:
logging.warn('one time auth fail, drop chunk !')
else:
@ -477,7 +493,7 @@ class TCPRelayHandler(object):
def _ota_chunk_data_gen(self, data):
data_len = struct.pack(">H", len(data))
index = struct.pack('>I', self._ota_chunk_idx)
key = self._encryptor.cipher_iv + index
key = self._cryptor.cipher_iv + index
sha110 = onetimeauth_gen(data, key)
self._ota_chunk_idx += 1
return data_len + sha110 + data
@ -486,7 +502,7 @@ class TCPRelayHandler(object):
if self._is_local:
if self._ota_enable_session:
data = self._ota_chunk_data_gen(data)
data = self._encryptor.encrypt(data)
data = self._cryptor.encrypt(data)
self._write_to_sock(data, self._remote_sock)
else:
if self._ota_enable_session:
@ -540,8 +556,12 @@ class TCPRelayHandler(object):
return
is_local = self._is_local
data = None
if is_local:
buf_size = UP_STREAM_BUF_SIZE
else:
buf_size = DOWN_STREAM_BUF_SIZE
try:
data = self._local_sock.recv(BUF_SIZE)
data = self._local_sock.recv(buf_size)
except (OSError, IOError) as e:
if eventloop.errno_from_exception(e) in \
(errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK):
@ -551,14 +571,19 @@ class TCPRelayHandler(object):
return
self._update_activity(len(data))
if not is_local:
data = self._encryptor.decrypt(data)
data = self._cryptor.decrypt(data)
if not data:
return
if self._stage == STAGE_STREAM:
self._handle_stage_stream(data)
return
elif is_local and self._stage == STAGE_INIT:
self._handle_stage_init(data)
# jump over socks5 init
if self._is_tunnel:
self._handle_stage_addr(data)
return
else:
self._handle_stage_init(data)
elif self._stage == STAGE_CONNECTING:
self._handle_stage_connecting(data)
elif (is_local and self._stage == STAGE_ADDR) or \
@ -568,8 +593,12 @@ class TCPRelayHandler(object):
def _on_remote_read(self):
# handle all remote read events
data = None
if self._is_local:
buf_size = UP_STREAM_BUF_SIZE
else:
buf_size = DOWN_STREAM_BUF_SIZE
try:
data = self._remote_sock.recv(BUF_SIZE)
data = self._remote_sock.recv(buf_size)
except (OSError, IOError) as e:
if eventloop.errno_from_exception(e) in \
@ -580,9 +609,9 @@ class TCPRelayHandler(object):
return
self._update_activity(len(data))
if self._is_local:
data = self._encryptor.decrypt(data)
data = self._cryptor.decrypt(data)
else:
data = self._encryptor.encrypt(data)
data = self._cryptor.encrypt(data)
try:
self._write_to_sock(data, self._local_sock)
except Exception as e:
@ -623,6 +652,7 @@ class TCPRelayHandler(object):
logging.error(eventloop.get_sock_error(self._remote_sock))
self.destroy()
@shell.exception_handle(self_=True, destroy=True)
def handle_event(self, sock, event):
# handle all events in this handler and dispatch them to methods
if self._stage == STAGE_DESTROYED:
@ -689,6 +719,7 @@ class TCPRelayHandler(object):
class TCPRelay(object):
def __init__(self, config, dns_resolver, is_local, stat_callback=None):
self._config = config
self._is_local = is_local
@ -696,6 +727,7 @@ class TCPRelay(object):
self._closed = False
self._eventloop = None
self._fd_to_handlers = {}
self._is_tunnel = False
self._timeout = config['timeout']
self._timeouts = [] # a list for all the handlers

74
shadowsocks/tunnel.py Executable file
View file

@ -0,0 +1,74 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2012-2015 clowwindy
#
# 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
#
# http://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.
from __future__ import absolute_import, division, print_function, \
with_statement
import sys
import os
import logging
import signal
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))
from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns
@shell.exception_handle(self_=False, exit_code=1)
def main():
shell.check_python()
# fix py2exe
if hasattr(sys, "frozen") and sys.frozen in \
("windows_exe", "console_exe"):
p = os.path.dirname(os.path.abspath(sys.executable))
os.chdir(p)
config = shell.get_config(True)
daemon.daemon_exec(config)
dns_resolver = asyncdns.DNSResolver()
loop = eventloop.EventLoop()
dns_resolver.add_to_loop(loop)
_config = config.copy()
_config["local_port"] = _config["tunnel_port"]
logging.info("starting tcp tunnel at %s:%d forward to %s:%d" %
(_config['local_address'], _config['local_port'],
_config['tunnel_remote'], _config['tunnel_remote_port']))
tunnel_tcp_server = tcprelay.TCPRelay(_config, dns_resolver, True)
tunnel_tcp_server._is_tunnel = True
tunnel_tcp_server.add_to_loop(loop)
logging.info("starting udp tunnel at %s:%d forward to %s:%d" %
(_config['local_address'], _config['local_port'],
_config['tunnel_remote'], _config['tunnel_remote_port']))
tunnel_udp_server = udprelay.UDPRelay(_config, dns_resolver, True)
tunnel_udp_server._is_tunnel = True
tunnel_udp_server.add_to_loop(loop)
def handler(signum, _):
logging.warn('received SIGQUIT, doing graceful shutting down..')
tunnel_tcp_server.close(next_tick=True)
tunnel_udp_server.close(next_tick=True)
signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler)
def int_handler(signum, _):
sys.exit(1)
signal.signal(signal.SIGINT, int_handler)
daemon.set_user(config.get('user', None))
loop.run()
if __name__ == '__main__':
main()

View file

@ -68,7 +68,7 @@ import struct
import errno
import random
from shadowsocks import encrypt, eventloop, lru_cache, common, shell
from shadowsocks import cryptor, eventloop, lru_cache, common, shell
from shadowsocks.common import parse_header, pack_addr, onetimeauth_verify, \
onetimeauth_gen, ONETIMEAUTH_BYTES, ADDRTYPE_AUTH
@ -82,6 +82,7 @@ def client_key(source_addr, server_af):
class UDPRelay(object):
def __init__(self, config, dns_resolver, is_local, stat_callback=None):
self._config = config
if is_local:
@ -94,6 +95,10 @@ class UDPRelay(object):
self._listen_port = config['server_port']
self._remote_addr = None
self._remote_port = None
self.tunnel_remote = config.get('tunnel_remote', "8.8.8.8")
self.tunnel_remote_port = config.get('tunnel_remote_port', 53)
self.tunnel_port = config.get('tunnel_port', 53)
self._is_tunnel = False
self._dns_resolver = dns_resolver
self._password = common.to_bytes(config['password'])
self._method = config['method']
@ -110,6 +115,8 @@ class UDPRelay(object):
self._closed = False
self._sockets = set()
self._forbidden_iplist = config.get('forbidden_ip')
self._crypto_path = config['crypto_path']
addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0,
socket.SOCK_DGRAM, socket.SOL_UDP)
if len(addrs) == 0:
@ -151,27 +158,37 @@ class UDPRelay(object):
if self._stat_callback:
self._stat_callback(self._listen_port, len(data))
if self._is_local:
frag = common.ord(data[2])
if frag != 0:
logging.warn('UDP drop a message since frag is not 0')
return
if self._is_tunnel:
# add ss header to data
tunnel_remote = self.tunnel_remote
tunnel_remote_port = self.tunnel_remote_port
data = common.add_header(tunnel_remote,
tunnel_remote_port, data)
else:
data = data[3:]
frag = common.ord(data[2])
if frag != 0:
logging.warn('UDP drop a message since frag is not 0')
return
else:
data = data[3:]
else:
data, key, iv = encrypt.dencrypt_all(self._password,
self._method,
data)
# decrypt data
try:
data, key, iv = cryptor.decrypt_all(self._password,
self._method,
data, self._crypto_path)
except Exception:
logging.debug('UDP handle_server: decrypt data failed')
return
if not data:
logging.debug(
'UDP handle_server: data is empty after decrypt'
)
logging.debug('UDP handle_server: data is empty after decrypt')
return
header_result = parse_header(data)
if header_result is None:
return
addrtype, dest_addr, dest_port, header_length = header_result
logging.info("udp data to %s:%d from %s:%d"
% (dest_addr, dest_port, r_addr[0], r_addr[1]))
if self._is_local:
server_addr, server_port = self._get_a_server()
else:
@ -221,11 +238,16 @@ class UDPRelay(object):
self._eventloop.add(client, eventloop.POLL_IN, self)
if self._is_local:
key, iv, m = encrypt.gen_key_iv(self._password, self._method)
key, iv, m = cryptor.gen_key_iv(self._password, self._method)
# spec https://shadowsocks.org/en/spec/one-time-auth.html
if self._ota_enable_session:
data = self._ota_chunk_data_gen(key, iv, data)
data = encrypt.encrypt_all_m(key, iv, m, self._method, data)
try:
data = cryptor.encrypt_all_m(key, iv, m, self._method, data,
self._crypto_path)
except Exception:
logging.debug("UDP handle_server: encrypt data failed")
return
if not data:
return
else:
@ -254,22 +276,38 @@ class UDPRelay(object):
# drop
return
data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data
response = encrypt.encrypt_all(self._password, self._method, 1,
data)
try:
response = cryptor.encrypt_all(self._password,
self._method, data,
self._crypto_path)
except Exception:
logging.debug("UDP handle_client: encrypt data failed")
return
if not response:
return
else:
data = encrypt.encrypt_all(self._password, self._method, 0,
data)
try:
data, key, iv = cryptor.decrypt_all(self._password,
self._method, data,
self._crypto_path)
except Exception:
logging.debug('UDP handle_client: decrypt data failed')
return
if not data:
return
header_result = parse_header(data)
if header_result is None:
return
addrtype, dest_addr, dest_port, header_length = header_result
response = b'\x00\x00\x00' + data
if self._is_tunnel:
# remove ss header
response = data[header_length:]
else:
response = b'\x00\x00\x00' + data
client_addr = self._client_fd_to_server_addr.get(sock.fileno())
if client_addr:
logging.debug("send udp response to %s:%d"
% (client_addr[0], client_addr[1]))
self._server_socket.sendto(response, client_addr)
else:
# this packet is from somewhere else we know

23
snapcraft.yaml Normal file
View file

@ -0,0 +1,23 @@
name: shadowsocks
version: 2.9.1-1
summary: A fast tunnel proxy that helps you bypass firewalls
description: A fast tunnel proxy that helps you bypass firewalls
confinement: strict
grade: stable
apps:
sslocal:
command: bin/sslocal
plugs: [network, network-bind]
aliases: [sslocal]
ssserver:
command: bin/ssserver
plugs: [network, network-bind]
aliases: [ssserver]
parts:
shadowsocks:
plugin: python
python-version: python2
source: https://github.com/shadowsocks/shadowsocks/archive/2.9.1.tar.gz

10
tests/aes-gcm.json Normal file
View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"aes-256-gcm",
"local_address":"127.0.0.1",
"fast_open":false
}

11
tests/aes-ocb.json Normal file
View file

@ -0,0 +1,11 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"aes-256-ocb",
"local_address":"127.0.0.1",
"fast_open":false,
"libopenssl":"/usr/local/lib/libcrypto.so.1.1"
}

10
tests/aes-ofb.json Normal file
View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"aes-256-ofb",
"local_address":"127.0.0.1",
"fast_open":false
}

10
tests/camellia.json Normal file
View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"camellia_password",
"timeout":60,
"method":"camellia-256-cfb",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"salsa20_password",
"timeout":60,
"method":"chacha20-ietf-poly1305",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"salsa20_password",
"timeout":60,
"method":"chacha20-poly1305",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -2,7 +2,7 @@
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"salsa20_password",
"password":"chacha20_password",
"timeout":60,
"method":"chacha20",
"local_address":"127.0.0.1",

View file

@ -33,12 +33,25 @@ run_test coverage run tests/nose_plugin.py -v
run_test python setup.py sdist
run_test tests/test_daemon.sh
run_test python tests/test.py --with-coverage -c tests/aes.json
run_test python tests/test.py --with-coverage -c tests/mbedtls-aes.json
run_test python tests/test.py --with-coverage -c tests/aes-gcm.json
run_test python tests/test.py --with-coverage -c tests/aes-ocb.json
run_test python tests/test.py --with-coverage -c tests/mbedtls-aes-gcm.json
run_test python tests/test.py --with-coverage -c tests/aes-ctr.json
run_test python tests/test.py --with-coverage -c tests/mbedtls-aes-ctr.json
run_test python tests/test.py --with-coverage -c tests/aes-cfb1.json
run_test python tests/test.py --with-coverage -c tests/aes-cfb8.json
run_test python tests/test.py --with-coverage -c tests/aes-ofb.json
run_test python tests/test.py --with-coverage -c tests/camellia.json
run_test python tests/test.py --with-coverage -c tests/mbedtls-camellia.json
run_test python tests/test.py --with-coverage -c tests/rc4-md5.json
run_test python tests/test.py --with-coverage -c tests/salsa20.json
run_test python tests/test.py --with-coverage -c tests/chacha20.json
run_test python tests/test.py --with-coverage -c tests/xchacha20.json
run_test python tests/test.py --with-coverage -c tests/chacha20-ietf.json
run_test python tests/test.py --with-coverage -c tests/chacha20-poly1305.json
run_test python tests/test.py --with-coverage -c tests/xchacha20-ietf-poly1305.json
run_test python tests/test.py --with-coverage -c tests/chacha20-ietf-poly1305.json
run_test python tests/test.py --with-coverage -c tests/table.json
run_test python tests/test.py --with-coverage -c tests/server-multi-ports.json
run_test python tests/test.py --with-coverage -s tests/aes.json -c tests/client-multi-server-ip.json
@ -52,6 +65,15 @@ run_test python tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0
run_test python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --workers 1" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -qq -b 127.0.0.1"
run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=127.0.0.1,::1,8.8.8.8" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1"
# test custom lib path
run_test python tests/test.py --with-coverage --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libopenssl=/usr/local/lib/libcrypto.so" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libopenssl=/usr/local/lib/libcrypto.so"
run_test python tests/test.py --with-coverage --url="http://127.0.0.1/" -b "-m mbedtls:aes-256-cfb128 -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libmbedtls=/usr/local/lib/libmbedcrypto.so" -a "-m mbedtls:aes-256-cfb128 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libmbedtls=/usr/local/lib/libmbedcrypto.so"
run_test python tests/test.py --with-coverage --url="http://127.0.0.1/" -b "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libsodium=/usr/local/lib/libsodium.so" -a "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libsodium=/usr/local/lib/libsodium.so"
run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libopenssl=invalid_path" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libopenssl=invalid_path"
run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libsodium=invalid_path" -a "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libsodium=invalid_path"
run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m mbedtls:aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libmbedtls=invalid_path" -a "-m mbedtls:aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libmbedtls=invalid_path"
# test if DNS works
run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204"
@ -80,11 +102,10 @@ fi
run_test tests/test_udp_src.sh
run_test tests/test_command.sh
coverage combine && coverage report --include=shadowsocks/*
rm -rf htmlcov
rm -rf tmp
coverage html --include=shadowsocks/*
coverage report --include=shadowsocks/* | tail -n1 | rev | cut -d' ' -f 1 | rev > /tmp/shadowsocks-coverage
# coverage combine && coverage report --include=shadowsocks/*
# rm -rf htmlcov
# rm -rf tmp
# coverage html --include=shadowsocks/*
# coverage report --include=shadowsocks/* | tail -n1 | rev | cut -d' ' -f 1 | rev > /tmp/shadowsocks-coverage
exit $result

12
tests/libmbedtls/install.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
MBEDTLS_VER=2.4.2
if [ ! -d mbedtls-$MBEDTLS_VER ]; then
wget https://tls.mbed.org/download/mbedtls-$MBEDTLS_VER-gpl.tgz || exit 1
tar xf mbedtls-$MBEDTLS_VER-gpl.tgz || exit 1
fi
pushd mbedtls-$MBEDTLS_VER
make SHARED=1 CFLAGS=-fPIC && sudo make install || exit 1
sudo ldconfig
popd
rm -rf mbedtls-$MBEDTLS_VER || exit 1

19
tests/libopenssl/install.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
OPENSSL_VER=1.1.0e
if [ ! -d openssl-$OPENSSL_VER ]; then
wget https://www.openssl.org/source/openssl-$OPENSSL_VER.tar.gz || exit 1
tar xf openssl-$OPENSSL_VER.tar.gz || exit 1
fi
pushd openssl-$OPENSSL_VER
./config && make && sudo make install || exit 1
# sudo ldconfig # test multiple libcrypto
popd
rm -rf openssl-$OPENSSL_VER || exit 1
rm /usr/bin/openssl || exit 1
rm -r /usr/include/openssl || exit 1
ln -s /usr/local/bin/openssl /usr/bin/openssl || exit 1
ln -s /usr/local/include/openssl /usr/include/openssl || exit 1
echo /usr/local/lib >> /etc/ld.so.conf || exit 1
ldconfig -v || exit 1

View file

@ -1,10 +1,11 @@
#!/bin/bash
if [ ! -d libsodium-1.0.11 ]; then
wget https://github.com/jedisct1/libsodium/releases/download/1.0.11/libsodium-1.0.11.tar.gz || exit 1
tar xf libsodium-1.0.11.tar.gz || exit 1
if [ ! -d libsodium-1.0.12 ]; then
wget https://github.com/jedisct1/libsodium/releases/download/1.0.12/libsodium-1.0.12.tar.gz || exit 1
tar xf libsodium-1.0.12.tar.gz || exit 1
fi
pushd libsodium-1.0.11
pushd libsodium-1.0.12
./configure && make -j2 && make install || exit 1
sudo ldconfig
popd
rm -rf libsodium-1.0.12 || exit 1

View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"mbedtls:aes-256-ctr",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"mbedtls:aes-256-gcm",
"local_address":"127.0.0.1",
"fast_open":false
}

10
tests/mbedtls-aes.json Normal file
View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"mbedtls:aes-256-cfb128",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"camellia_password",
"timeout":60,
"method":"mbedtls:camellia-256-cfb128",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"salsa20_password",
"timeout":60,
"method":"xchacha20-ietf-poly1305",
"local_address":"127.0.0.1",
"fast_open":false
}

10
tests/xchacha20.json Normal file
View file

@ -0,0 +1,10 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"xchacha20_password",
"timeout":60,
"method":"xchacha20",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -24,9 +24,17 @@
from __future__ import absolute_import, division, print_function, \
with_statement
import os
import sys
import socket
import argparse
import subprocess
def inet_pton(str_ip):
try:
return socket.inet_pton(socket.AF_INET, str_ip)
except socket.error:
return None
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='See README')
@ -37,17 +45,22 @@ if __name__ == '__main__':
ips = {}
banned = set()
for line in sys.stdin:
if 'can not parse header when' in line:
ip = line.split()[-1].split(':')[-2]
if ip not in ips:
ips[ip] = 1
print(ip)
sys.stdout.flush()
else:
ips[ip] += 1
if ip not in banned and ips[ip] >= config.count:
banned.add(ip)
cmd = 'iptables -A INPUT -s %s -j DROP' % ip
print(cmd, file=sys.stderr)
sys.stderr.flush()
os.system(cmd)
if 'can not parse header when' not in line:
continue
ip_str = line.split()[-1].rsplit(':', 1)[0]
ip = inet_pton(ip_str)
if ip is None:
continue
if ip not in ips:
ips[ip] = 1
sys.stdout.flush()
else:
ips[ip] += 1
if ip not in banned and ips[ip] >= config.count:
banned.add(ip)
print('ban ip %s' % ip_str)
cmd = ['iptables', '-A', 'INPUT', '-s', ip_str, '-j', 'DROP',
'-m', 'comment', '--comment', 'autoban']
print(' '.join(cmd), file=sys.stderr)
sys.stderr.flush()
subprocess.call(cmd)