Skip to content

Commit 956199e

Browse files
author
clowwindy
committed
add control manager
1 parent 58df1d8 commit 956199e

File tree

5 files changed

+170
-3
lines changed

5 files changed

+170
-3
lines changed

shadowsocks/manager.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright 2015 clowwindy
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
7+
# not use this file except in compliance with the License. You may obtain
8+
# a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
# License for the specific language governing permissions and limitations
16+
# under the License.
17+
18+
from __future__ import absolute_import, division, print_function, \
19+
with_statement
20+
21+
import errno
22+
import traceback
23+
import socket
24+
import logging
25+
import json
26+
import collections
27+
28+
from shadowsocks import common, eventloop, tcprelay, udprelay, asyncdns, shell
29+
30+
31+
BUF_SIZE = 2048
32+
33+
34+
class Manager(object):
35+
36+
def __init__(self, config):
37+
self._config = config
38+
self._relays = {} # (tcprelay, udprelay)
39+
self._loop = eventloop.EventLoop()
40+
self._dns_resolver = asyncdns.DNSResolver()
41+
self._dns_resolver.add_to_loop(self._loop)
42+
self._control_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
43+
socket.IPPROTO_UDP)
44+
self._statistics = collections.defaultdict(int)
45+
self._control_client_addr = None
46+
try:
47+
self._control_socket.bind(('127.0.0.1',
48+
int(config['manager_port'])))
49+
self._control_socket.setblocking(False)
50+
except (OSError, IOError) as e:
51+
logging.error(e)
52+
logging.error('can not bind to manager port')
53+
exit(1)
54+
self._loop.add(self._control_socket,
55+
eventloop.POLL_IN, self)
56+
57+
port_password = config['port_password']
58+
del config['port_password']
59+
for port, password in port_password.items():
60+
a_config = config.copy()
61+
a_config['server_port'] = int(port)
62+
a_config['password'] = password
63+
self.add_port(a_config)
64+
65+
def add_port(self, config):
66+
port = int(config['server_port'])
67+
servers = self._relays.get(port, None)
68+
if servers:
69+
logging.error("server already exists at %s:%d" % (config['server'],
70+
port))
71+
return
72+
logging.info("adding server at %s:%d" % (config['server'], port))
73+
t = tcprelay.TCPRelay(config, self._dns_resolver, False)
74+
u = udprelay.UDPRelay(config, self._dns_resolver, False)
75+
t.add_to_loop(self._loop)
76+
u.add_to_loop(self._loop)
77+
self._relays[port] = (t, u)
78+
79+
def remove_port(self, config):
80+
port = int(config['server_port'])
81+
servers = self._relays.get(port, None)
82+
if servers:
83+
logging.info("removing server at %s:%d" % (config['server'], port))
84+
t, u = servers
85+
t.close(next_tick=False)
86+
u.close(next_tick=False)
87+
del self._relays[port]
88+
else:
89+
logging.error("server not exist at %s:%d" % (config['server'],
90+
port))
91+
92+
def handle_event(self, sock, fd, event):
93+
if sock == self._control_socket and event == eventloop.POLL_IN:
94+
data, self._control_client_addr = sock.recvfrom(BUF_SIZE)
95+
parsed = self._parse_command(data)
96+
if parsed:
97+
command, config = parsed
98+
a_config = self._config.copy()
99+
if config:
100+
a_config.update(config)
101+
if 'server_port' not in a_config:
102+
logging.error('can not find server_port in config')
103+
else:
104+
if command == 'add':
105+
self.add_port(a_config)
106+
elif command == 'remove':
107+
self.remove_port(a_config)
108+
elif command == 'ping':
109+
self._send_control_data(b'pong')
110+
else:
111+
logging.error('unknown command %s', command)
112+
113+
def _parse_command(self, data):
114+
# commands:
115+
# add: {"server_port": 8000, "password": "foobar"}
116+
# remove: {"server_port": 8000"}
117+
data = common.to_str(data)
118+
parts = data.split(':', 1)
119+
if len(parts) < 2:
120+
return data, None
121+
command, config_json = parts
122+
try:
123+
config = json.loads(config_json)
124+
return command, config
125+
except Exception as e:
126+
logging.error(e)
127+
return None
128+
129+
def handle_periodic(self):
130+
# TODO send statistics
131+
pass
132+
133+
def _send_control_data(self, data):
134+
if self._control_client_addr:
135+
try:
136+
self._control_socket.sendto(data, self._control_client_addr)
137+
except (socket.error, OSError, IOError) as e:
138+
error_no = eventloop.errno_from_exception(e)
139+
if error_no in (errno.EAGAIN, errno.EINPROGRESS,
140+
errno.EWOULDBLOCK):
141+
return
142+
else:
143+
shell.print_exception(e)
144+
if self._config['verbose']:
145+
traceback.print_exc()
146+
147+
def run(self):
148+
self._loop.run()
149+
150+
151+
def run(config):
152+
Manager(config).run()

shadowsocks/server.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
import signal
2525

2626
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))
27-
from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns
27+
from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, \
28+
asyncdns, manager
2829

2930

3031
def main():
@@ -48,10 +49,17 @@ def main():
4849
else:
4950
config['port_password'][str(server_port)] = config['password']
5051

52+
if config['manager_port']:
53+
logging.info('entering manager mode')
54+
manager.run(config)
55+
return
56+
5157
tcp_servers = []
5258
udp_servers = []
5359
dns_resolver = asyncdns.DNSResolver()
54-
for port, password in config['port_password'].items():
60+
port_password = config['port_password']
61+
del config['port_password']
62+
for port, password in port_password.items():
5563
a_config = config.copy()
5664
a_config['server_port'] = int(port)
5765
a_config['password'] = password

shadowsocks/shell.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def get_config(is_local):
136136
else:
137137
shortopts = 'hd:s:p:k:m:c:t:vq'
138138
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=',
139-
'forbidden-ip=', 'user=', 'version']
139+
'forbidden-ip=', 'user=', 'manager-port=', 'version']
140140
try:
141141
config_path = find_config()
142142
optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
@@ -181,6 +181,8 @@ def get_config(is_local):
181181
config['fast_open'] = True
182182
elif key == '--workers':
183183
config['workers'] = int(value)
184+
elif key == '--manager-port':
185+
config['manager_port'] = int(value)
184186
elif key == '--user':
185187
config['user'] = to_str(value)
186188
elif key == '--forbidden-ip':
@@ -317,6 +319,7 @@ def print_server_help():
317319
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
318320
--workers WORKERS number of workers, available on Unix/Linux
319321
--forbidden-ip IPLIST comma seperated IP list forbidden to connect
322+
--manager-port PORT optional server manager UDP port
320323
321324
General options:
322325
-h, --help show this help message and exit

shadowsocks/tcprelay.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,3 +708,5 @@ def close(self, next_tick=False):
708708
self._eventloop.remove_periodic(self.handle_periodic)
709709
self._eventloop.remove(self._server_socket)
710710
self._server_socket.close()
711+
for handler in list(self._fd_to_handlers.values()):
712+
handler.destroy()

shadowsocks/udprelay.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,5 @@ def close(self, next_tick=False):
290290
self._eventloop.remove_periodic(self.handle_periodic)
291291
self._eventloop.remove(self._server_socket)
292292
self._server_socket.close()
293+
for client in list(self._cache.values()):
294+
client.close()

0 commit comments

Comments
 (0)