mirror of
https://github.com/hanwckf/immortalwrt-mt798x.git
synced 2025-01-10 03:09:08 +08:00
luci-app-ssr-mudb-server: add package
This commit is contained in:
parent
7ddf50e9c3
commit
95ffebad01
16
package/lienol/luci-app-ssr-mudb-server/Makefile
Normal file
16
package/lienol/luci-app-ssr-mudb-server/Makefile
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright (C) 2018-2020 Lienol <lawlienol@gmail.com>
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v3.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for SSR Mudb Server
|
||||
LUCI_DEPENDS:=+libsodium +luci-lib-jsonc +python3
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_VERSION:=10
|
||||
PKG_DATE:=20200619
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
6
package/lienol/luci-app-ssr-mudb-server/htdocs/luci-static/resources/vue.min.js
vendored
Normal file
6
package/lienol/luci-app-ssr-mudb-server/htdocs/luci-static/resources/vue.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,190 @@
|
||||
-- Copyright 2018-2020 Lienol <lawlienol@gmail.com>
|
||||
module("luci.controller.ssr_mudb_server", package.seeall)
|
||||
local http = require "luci.http"
|
||||
local jsonc = require "luci.jsonc"
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/ssr_mudb_server") then return end
|
||||
entry({"admin", "vpn"}, firstchild(), "VPN", 45).dependent = false
|
||||
if nixio.fs.access("/usr/share/ssr_mudb_server") then
|
||||
entry({"admin", "vpn", "ssr_mudb_server"}, cbi("ssr_mudb_server/index"), _("SSR MuDB Server"), 2).dependent = true
|
||||
end
|
||||
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "user"}, template("ssr_mudb_server/user")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "config"}, cbi("ssr_mudb_server/config")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "status"}, call("status")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "user_save"}, call("user_save")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "user_list"}, call("user_list")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "user_get"}, call("user_get")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "get_link"}, call("get_link")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "clear_traffic"}, call("clear_traffic")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "clear_traffic_all_users"}, call("clear_traffic_all_users")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "remove_user"}, call("remove_user")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "get_log"}, call("get_log")).leaf = true
|
||||
entry({"admin", "vpn", "ssr_mudb_server", "clear_log"}, call("clear_log")).leaf = true
|
||||
end
|
||||
|
||||
local function http_write_json(content)
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(content or {code = 1})
|
||||
end
|
||||
|
||||
local function get_config_path()
|
||||
return luci.sys.exec("echo -n $(cat /usr/share/ssr_mudb_server/userapiconfig.py | grep 'MUDB_FILE' | cut -d \"'\" -f 2)")
|
||||
end
|
||||
|
||||
local function get_config_json()
|
||||
return luci.sys.exec("cat " .. get_config_path()) or "[]"
|
||||
end
|
||||
|
||||
function status()
|
||||
local e = {}
|
||||
e.status = luci.sys.call("ps -w | grep -v grep | grep '/usr/share/ssr_mudb_server/server.py' >/dev/null") == 0
|
||||
http_write_json(e)
|
||||
end
|
||||
|
||||
function get_link()
|
||||
local e = {}
|
||||
local link = luci.sys.exec("cd /usr/share/ssr_mudb_server && python3 mujson_mgr.py -l -p " .. luci.http.formvalue("port") .. " | sed -n '$p'"):gsub("^%s*(.-)%s*$", "%1")
|
||||
if link ~= "" then e.link = link end
|
||||
http_write_json(e)
|
||||
end
|
||||
|
||||
function clear_traffic()
|
||||
local e = {}
|
||||
e.status = luci.sys.call("cd /usr/share/ssr_mudb_server && python3 mujson_mgr.py -c -p '" .. luci.http.formvalue("port") .. "' >/dev/null") == 0
|
||||
http_write_json(e)
|
||||
end
|
||||
|
||||
function clear_traffic_all_users()
|
||||
local e = {}
|
||||
e.status = luci.sys.call("/usr/share/ssr_mudb_server/clear_traffic_all_users.sh >/dev/null") == 0
|
||||
http_write_json(e)
|
||||
end
|
||||
|
||||
function user_list()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write(get_config_json())
|
||||
end
|
||||
|
||||
function user_save()
|
||||
local result = {code = 0}
|
||||
local action = luci.http.formvalue("action")
|
||||
local json_str = luci.http.formvalue("json")
|
||||
if action and action == "add" or action == "edit" then
|
||||
local user = jsonc.parse(json_str)
|
||||
if user then
|
||||
local json = jsonc.parse(get_config_json())
|
||||
if json then
|
||||
local port = user.port
|
||||
local is_exist_port = 0
|
||||
for index = 1, table.maxn(json) do
|
||||
if json[index].port == tonumber(port) then
|
||||
is_exist_port = 1
|
||||
if user.old_port and user.old_port == tonumber(port) then
|
||||
is_exist_port = 0
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
if is_exist_port == 0 then
|
||||
if action == "add" then
|
||||
local new_user = {
|
||||
enable = tonumber(user.enable) or 0,
|
||||
user = user.user or user.port,
|
||||
port = tonumber(user.port),
|
||||
passwd = user.passwd,
|
||||
method = user.method,
|
||||
protocol = user.protocol,
|
||||
obfs = user.obfs,
|
||||
protocol_param = user.protocol_param or "2",
|
||||
speed_limit_per_con = tonumber(user.speed_limit_per_con) or 0,
|
||||
speed_limit_per_user = tonumber(user.speed_limit_per_user) or 0,
|
||||
forbidden_port = user.forbidden_port or "",
|
||||
transfer_enable = tonumber(user.transfer_enable) or 1073741824,
|
||||
d = 0,
|
||||
u = 0
|
||||
}
|
||||
table.insert(json, new_user)
|
||||
elseif action == "edit" then
|
||||
for index = 1, table.maxn(json) do
|
||||
if json[index].port == tonumber(user.old_port) then
|
||||
json[index].enable = tonumber(user.enable) or 0
|
||||
json[index].user = user.user
|
||||
json[index].port = tonumber(user.port)
|
||||
json[index].passwd = user.passwd
|
||||
json[index].method = user.method
|
||||
json[index].protocol = user.protocol
|
||||
json[index].obfs = user.obfs
|
||||
json[index].protocol_param = user.protocol_param
|
||||
json[index].speed_limit_per_con = tonumber(user.speed_limit_per_con)
|
||||
json[index].speed_limit_per_user = tonumber(user.speed_limit_per_user)
|
||||
json[index].forbidden_port = user.forbidden_port
|
||||
json[index].transfer_enable = tonumber(user.transfer_enable)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local f, err = io.open(get_config_path(), "w")
|
||||
if f and err == nil then
|
||||
f:write(jsonc.stringify(json, 1))
|
||||
f:close()
|
||||
luci.sys.call("/etc/init.d/ssr_mudb_server restart")
|
||||
result = {code = 1}
|
||||
end
|
||||
else
|
||||
result.msg = "端口已存在!"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
http_write_json(result)
|
||||
end
|
||||
|
||||
function user_get()
|
||||
local result = {}
|
||||
local port = luci.http.formvalue("port")
|
||||
local str = get_config_json()
|
||||
local json = jsonc.parse(str)
|
||||
if port and str and json then
|
||||
for index = 1, table.maxn(json) do
|
||||
local o = json[index]
|
||||
if o.port == tonumber(port) then
|
||||
result = o
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
http_write_json(result)
|
||||
end
|
||||
|
||||
function remove_user()
|
||||
local port = luci.http.formvalue("port")
|
||||
local str = get_config_json()
|
||||
local json = jsonc.parse(str)
|
||||
if port and str and json then
|
||||
for index = 1, table.maxn(json) do
|
||||
local o = json[index]
|
||||
if o.port == tonumber(port) then
|
||||
json[index] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
local f, err = io.open(get_config_path(), "w")
|
||||
if f and err == nil then
|
||||
f:write(jsonc.stringify(json, 1))
|
||||
f:close()
|
||||
end
|
||||
luci.http.status = 200
|
||||
else
|
||||
luci.http.status = 500
|
||||
end
|
||||
end
|
||||
|
||||
function get_log()
|
||||
luci.http.write(luci.sys.exec("[ -f '/var/log/ssr_mudb_server.log' ] && cat /var/log/ssr_mudb_server.log"))
|
||||
end
|
||||
|
||||
function clear_log()
|
||||
luci.sys.call("echo '' > /var/log/ssr_mudb_server.log")
|
||||
end
|
@ -0,0 +1,51 @@
|
||||
local appname = "ssr_mudb_server"
|
||||
local jsonc = require "luci.jsonc"
|
||||
|
||||
a = Map(appname, translate("ShadowsocksR MuDB Server"))
|
||||
|
||||
t = a:section(TypedSection, "global", translate("Global Settings"))
|
||||
t.anonymous = true
|
||||
t.addremove = false
|
||||
|
||||
t:append(Template(appname .. "/status"))
|
||||
|
||||
e = t:option(Flag, "enable", translate("Enable"))
|
||||
e.rmempty = false
|
||||
|
||||
e = t:option(Flag, "auto_clear_transfer", translate("Enable Auto Clear Traffic"))
|
||||
e.default = 0
|
||||
e.rmempty = false
|
||||
|
||||
e = t:option(Value, "auto_clear_transfer_time", translate("Clear Traffic Time Interval"), translate("*,*,*,*,* is Min Hour Day Mon Week"))
|
||||
e.default = "0,2,1,*,*"
|
||||
e:depends("auto_clear_transfer", 1)
|
||||
|
||||
a:append(Template(appname .. "/users"))
|
||||
|
||||
local apply = luci.http.formvalue("cbi.apply")
|
||||
if apply then
|
||||
for key, val in pairs(luci.http.formvalue()) do
|
||||
if key:find("ssr_mudb_server_") == 1 then
|
||||
local port = key:gsub("ssr_mudb_server_",""):gsub(".enable","")
|
||||
local config_path = luci.sys.exec("echo -n $(cat /usr/share/ssr_mudb_server/userapiconfig.py | grep 'MUDB_FILE' | cut -d \"'\" -f 2)")
|
||||
local str = luci.sys.exec("cat " .. config_path) or "[]"
|
||||
local json = jsonc.parse(str)
|
||||
if port and str and json then
|
||||
for index = 1, table.maxn(json) do
|
||||
local o = json[index]
|
||||
if o.port == tonumber(port) then
|
||||
json[index].enable = tonumber(val)
|
||||
break
|
||||
end
|
||||
end
|
||||
local f, err = io.open(config_path, "w")
|
||||
if f and err == nil then
|
||||
f:write(jsonc.stringify(json, 1))
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return a
|
@ -0,0 +1,26 @@
|
||||
<%
|
||||
local ipkg = require "luci.model.ipkg"
|
||||
-%>
|
||||
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title"><%:Status%></label>
|
||||
<div class="cbi-value-field" id="run_status" style="font-weight: bold;"><%:Collecting data...%></div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var run_status = document.getElementById('run_status');
|
||||
var style = run_status.getAttribute("style");
|
||||
<% if ipkg.installed("python3") then %>
|
||||
XHR.poll(2, '<%=luci.dispatcher.build_url("admin", "vpn", "ssr_mudb_server", "status")%>', null,
|
||||
function(x, result) {
|
||||
run_status.setAttribute("style", result.status ? style + "color: green;" : style + "color: red;");
|
||||
run_status.innerHTML = result.status ? '<%=translate("RUNNING")%>' : '<%=translate("NOT RUNNING")%>';
|
||||
}
|
||||
)
|
||||
<% else %>
|
||||
run_status.setAttribute("style", style + "color: red;");
|
||||
run_status.innerHTML = '<%=translate("NOT INSTALLED")%><br><%=translate("please Install the python3")%><br>opkg update && opkg install python3</font></b>';
|
||||
<% end %>
|
||||
//]]>
|
||||
</script>
|
@ -0,0 +1,258 @@
|
||||
<%+header%>
|
||||
<%
|
||||
-- Copyright (C) 2018-2020 Lienol <lawlienol@gmail.com>
|
||||
%>
|
||||
<script src="<%=resource%>/vue.min.js"></script>
|
||||
<form>
|
||||
<div class="cbi-map" id="ssr_mudb_server_user_div">
|
||||
<h2 name="content">ShadowsocksR MuDB {{title}}</h2>
|
||||
<fieldset class="cbi-section">
|
||||
<div class="cbi-section-node">
|
||||
|
||||
<div class="cbi-value" data-index="1" data-depends="[]">
|
||||
<label class="cbi-value-title">启用</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="hidden" id="enable" name="enable" v-model="user.enable">
|
||||
<input type="checkbox" :value="user.enable" @click="check_enable($event)"
|
||||
:checked="user.enable == 1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" data-index="2" data-depends="[]">
|
||||
<label class="cbi-value-title">备注</label>
|
||||
<div class="cbi-value-field">
|
||||
<input name="user" type="text" class="cbi-input-text" v-model="user.user">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" data-index="3" data-depends="[]">
|
||||
<label class="cbi-value-title">端口</label>
|
||||
<div class="cbi-value-field">
|
||||
<input name="port" type="text" class="cbi-input-text" v-model="user.port"
|
||||
onkeyup="this.value=this.value.replace(/[^0-9-]+/,'');">
|
||||
<input name="old_port" type="hidden" :value="user.old_port">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" data-index="4" data-depends="[]">
|
||||
<label class="cbi-value-title">密码</label>
|
||||
<div class="cbi-value-field">
|
||||
<input name="passwd" type="text" class="cbi-input-text" v-model="user.passwd"
|
||||
onkeyup="value=value.replace(/[\u4e00-\u9fa5]/ig,'')">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" data-index="5" data-depends="[]">
|
||||
<label class="cbi-value-title">加密</label>
|
||||
<div class="cbi-value-field">
|
||||
<select class="cbi-input-select" name="method" size="1" v-model="user.method">
|
||||
<option v-model="method" v-for="method in method_list">{{method}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" data-index="6" data-depends="[]">
|
||||
<label class="cbi-value-title">协议</label>
|
||||
<div class="cbi-value-field">
|
||||
<select class="cbi-input-select" name="protocol" size="1" v-model="user.protocol">
|
||||
<option v-model="protocol" v-for="protocol in protocol_list">{{protocol}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" data-index="7" data-depends="[]">
|
||||
<label class="cbi-value-title">混淆</label>
|
||||
<div class="cbi-value-field">
|
||||
<select class="cbi-input-select" name="obfs" size="1" v-model="user.obfs">
|
||||
<option v-model="obfs" v-for="obfs in obfs_list">{{obfs}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" data-index="8" data-depends="[]">
|
||||
<label class="cbi-value-title">设备数限制</label>
|
||||
<div class="cbi-value-field">
|
||||
<input name="device_limit" type="text" class="cbi-input-text" v-model="user.protocol_param"
|
||||
onkeyup="this.value=this.value.replace(/[^0-9-]+/,'');">
|
||||
<br>
|
||||
<div class="cbi-value-description">
|
||||
同一时间能链接的客户端数量(多端口模式,每个端口都是独立计算),建议最少 2个。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" data-index="9" data-depends="[]">
|
||||
<label class="cbi-value-title">单线程限速</label>
|
||||
<div class="cbi-value-field">
|
||||
<input name="speed_limit_per_con" type="text" class="cbi-input-text"
|
||||
v-model="user.speed_limit_per_con" onkeyup="this.value=this.value.replace(/[^0-9-]+/,'');">
|
||||
<br>
|
||||
<div class="cbi-value-description">
|
||||
单线程的限速上限,多线程即无效。0代表不限速。(单位:KB/S)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" data-index="10" data-depends="[]">
|
||||
<label class="cbi-value-title">总限速</label>
|
||||
<div class="cbi-value-field">
|
||||
<input name="speed_limit_per_user" type="text" class="cbi-input-text"
|
||||
v-model="user.speed_limit_per_user" onkeyup="this.value=this.value.replace(/[^0-9-]+/,'');">
|
||||
<br>
|
||||
<div class="cbi-value-description">
|
||||
总速度限速上限,单个端口整体限速。0代表不限速。(单位:KB/S)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value" data-index="11" data-depends="[]">
|
||||
<label class="cbi-value-title">禁止的端口</label>
|
||||
<div class="cbi-value-field">
|
||||
<input name="forbidden_port" type="text" class="cbi-input-text" v-model="user.forbidden_port">
|
||||
<br>
|
||||
<div class="cbi-value-description">
|
||||
例如不允许访问 25端口,用户就无法通过SSR代理访问邮件端口25了,如果禁止了 80,443 那么用户将无法正常访问 http/https 网站。<br>封禁单个端口格式:
|
||||
25<br>封禁多个端口格式: 23,465<br>封禁端口段格式: 233-266<br>封禁多种格式端口: 25,465,233-666
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-value cbi-value-last" data-index="12" data-depends="[]">
|
||||
<label class="cbi-value-title">可用总流量</label>
|
||||
<div class="cbi-value-field">
|
||||
<input name="transfer_enable" type="text" class="cbi-input-text" v-model="user.transfer_enable"
|
||||
onkeyup="this.value=this.value.replace(/[^0-9-]+/,'');">
|
||||
<br>
|
||||
<div class="cbi-value-description">
|
||||
可使用的总流量上限(单位: GB, 1-838868),0代表无限
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
</fieldset>
|
||||
<br>
|
||||
<div class="cbi-page-actions">
|
||||
<input class="cbi-button cbi-button-apply" type="button" value="保存并修改" @click="save()">
|
||||
<input class="cbi-button cbi-button-reset" type="button" value="返回" @click="goback()">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
var api_url = '<%=luci.dispatcher.build_url("admin", "vpn", "ssr_mudb_server")%>'
|
||||
var vue = new Vue({
|
||||
el: '#ssr_mudb_server_user_div',
|
||||
data: {
|
||||
title: '用户配置',
|
||||
user: {
|
||||
enable: 1,
|
||||
forbidden_port: "",
|
||||
method: "none",
|
||||
protocol: "auth_chain_a",
|
||||
obfs: "tls1.2_ticket_auth",
|
||||
protocol_param: "2",
|
||||
speed_limit_per_con: 0,
|
||||
speed_limit_per_user: 0,
|
||||
transfer_enable: 0,
|
||||
user: "起个名字吧"
|
||||
|
||||
},
|
||||
method_list: [
|
||||
"none",
|
||||
"table",
|
||||
"rc4",
|
||||
"rc4-md5",
|
||||
"aes-128-cfb",
|
||||
"aes-192-cfb",
|
||||
"aes-256-cfb",
|
||||
"aes-128-ctr",
|
||||
"aes-192-ctr",
|
||||
"aes-256-ctr",
|
||||
"bf-cfb",
|
||||
"cast5-cfb",
|
||||
"des-cfb",
|
||||
"rc2-cfb",
|
||||
"salsa20",
|
||||
"chacha20",
|
||||
"chacha20-ietf"
|
||||
],
|
||||
protocol_list: [
|
||||
"origin",
|
||||
"verify_simple",
|
||||
"verify_deflate",
|
||||
"verify_sha1",
|
||||
"auth_simple",
|
||||
"auth_sha1",
|
||||
"auth_sha1_v2",
|
||||
"auth_sha1_v4",
|
||||
"auth_aes128_md5",
|
||||
"auth_aes128_sha1",
|
||||
"auth_chain_a",
|
||||
"auth_chain_b",
|
||||
"auth_chain_c",
|
||||
"auth_chain_d"
|
||||
],
|
||||
obfs_list: [
|
||||
"plain",
|
||||
"http_simple",
|
||||
"http_post",
|
||||
"random_head",
|
||||
"tls_simple",
|
||||
"tls1.0_session_auth",
|
||||
"tls1.2_ticket_auth"
|
||||
]
|
||||
},
|
||||
methods: {
|
||||
check_enable: function (e) {
|
||||
if (e.currentTarget.checked == true) {
|
||||
this.user.enable = 1;
|
||||
} else {
|
||||
this.user.enable = 0;
|
||||
}
|
||||
},
|
||||
save: function () {
|
||||
var new_user = JSON.parse(JSON.stringify(this.user));
|
||||
if (new_user.port == null || isNaN(parseInt(new_user.port)) || parseInt(new_user.port) <= 0 || parseInt(new_user.port) >= 65536) {
|
||||
alert('端口必须是1-65535范围,且不能冲突!');
|
||||
return;
|
||||
}
|
||||
|
||||
new_user.transfer_enable = new_user.transfer_enable == 0 ? 838868 : new_user.transfer_enable;
|
||||
new_user.transfer_enable = (new_user.transfer_enable * 1024 * 1024 * 1024);
|
||||
XHR.get(api_url + "/user_save", { json: JSON.stringify(new_user), action: this.user.old_port ? "edit" : "add" },
|
||||
function (x, result) {
|
||||
if (x.status == 200) {
|
||||
if (result.code && result.code == 1) {
|
||||
window.location.href = api_url;
|
||||
} else {
|
||||
if (result.msg)
|
||||
alert(result.msg);
|
||||
else
|
||||
alert("未知错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
goback: function () {
|
||||
window.location.href = api_url;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var pathname = location.pathname;
|
||||
var port = pathname.substring(pathname.lastIndexOf('/') + 1);
|
||||
if (port && !isNaN(parseInt(port)) && parseInt(port) > 0 && parseInt(port) < 65536) {
|
||||
XHR.get(api_url + "/user_get", { port: port },
|
||||
function (x, result) {
|
||||
if (x.status == 200) {
|
||||
result.transfer_enable = (result.transfer_enable / 1024 / 1024 / 1024);
|
||||
result.old_port = result.port;
|
||||
vue.user = result;
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
<%+footer%>
|
@ -0,0 +1,144 @@
|
||||
<%
|
||||
-- Copyright (C) 2018-2020 Lienol <lawlienol@gmail.com>
|
||||
%>
|
||||
<script src="<%=resource%>/vue.min.js"></script>
|
||||
<fieldset class="cbi-section cbi-tblsection" id="users_table">
|
||||
<h3>{{title}}</h3>
|
||||
<div class="cbi-section-descr"></div>
|
||||
<table class="table cbi-section-table" style="">
|
||||
<tr class="tr cbi-section-table-titles anonymous">
|
||||
<th class="th cbi-section-table-cell" style="width:5%">启用</th>
|
||||
<th class="th cbi-section-table-cell" style="width:10%">备注</th>
|
||||
<th class="th cbi-section-table-cell" style="width:10%">端口</th>
|
||||
<th class="th cbi-section-table-cell" style="width:10%">禁止端口</th>
|
||||
<th class="th cbi-section-table-cell" style="width:10%">设备数限制</th>
|
||||
<th class="th cbi-section-table-cell" style="width:10%">单线程限速</th>
|
||||
<th class="th cbi-section-table-cell" style="width:10%">总限速</th>
|
||||
<th class="th cbi-section-table-cell" style="width:10%">已用流量</th>
|
||||
<th class="th cbi-section-table-cell" style="width:10%">可用流量</th>
|
||||
<!-- <th class="th cbi-section-table-cell" style="width:10%">SSR链接</th> -->
|
||||
<th class="th cbi-section-table-cell cbi-section-actions"></th>
|
||||
</tr>
|
||||
<template v-for="user in users">
|
||||
<tr class="tr cbi-section-table-row cbi-rowstyle-1">
|
||||
<td class="td cbi-value-field" data-name="enable" data-title="启用">
|
||||
<input type="hidden" :id="'ssr_mudb_server_' + user.port + '.enable'" :name="'ssr_mudb_server_' + user.port + '.enable'" :value="user.enable">
|
||||
<input type="checkbox" :value="user.enable" @click="check_enable(user, $event)" :checked="user.enable == 1">
|
||||
</td>
|
||||
|
||||
<td class="td cbi-value-field" data-name="remarks" data-title="备注">{{user.user}}</td>
|
||||
<td class="td cbi-value-field" data-name="port" data-title="端口">{{user.port}}</td>
|
||||
<td class="td cbi-value-field" data-name="forbidden_port" data-title="禁止端口">{{user.forbidden_port == '' ? '无' : user.forbidden_port}}</td>
|
||||
<td class="td cbi-value-field" data-name="device_limit" data-title="设备数限制">{{user.protocol_param}}</td>
|
||||
<td class="td cbi-value-field" data-name="speed_limit_per_con" data-title="单线程限速">{{user.speed_limit_per_con == '0' ? '无' : user.speed_limit_per_con + ' Kb/s'}}</td>
|
||||
<td class="td cbi-value-field" data-name="speed_limit_per_user" data-title="总限速">{{user.speed_limit_per_user == '0' ? '无' : user.speed_limit_per_user + ' Kb/s'}}</td>
|
||||
<td class="td cbi-value-field" data-name="used_total_traffic" data-title="已用流量">{{byte_format(user.u + user.d)}}</td>
|
||||
<td class="td cbi-value-field" data-name="transfer_enable" data-title="可用流量">{{byte_format(user.transfer_enable)}}</td>
|
||||
<!-- <td class="td cbi-value-field" data-name="ssr_link" data-title="SSR链接"><a href="#" @click="get_link(user)">获取</a></td> -->
|
||||
|
||||
<td class="td cbi-section-table-cell nowrap cbi-section-actions">
|
||||
<div>
|
||||
<input class="cbi-button cbi-button-remove" type="button" @click="clear_transfer(user)" value="清空流量">
|
||||
<input class="cbi-button cbi-button-edit" type="button" value="编辑" :onclick="'location.href=\'/cgi-bin/luci/admin/vpn/ssr_mudb_server/user/' + user.port + '\''">
|
||||
<input class="cbi-button cbi-button-remove" type="button" @click="remove(user)" value="删除">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
<div class="cbi-section-create cbi-tblsection-create">
|
||||
<input class="cbi-button cbi-button-add" type="button" value="添加" :onclick="'location.href=\'/cgi-bin/luci/admin/vpn/ssr_mudb_server/user\''">
|
||||
</div>
|
||||
<fieldset class="cbi-section" id="_log_fieldset">
|
||||
<legend>日志</legend>
|
||||
<input class="cbi-button cbi-input-remove" type="button" @click="clear_log()" value="清空日志">
|
||||
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" rows="20" wrap="off" readonly="readonly"></textarea>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
|
||||
<script>
|
||||
var api_url = '<%=luci.dispatcher.build_url("admin", "vpn", "ssr_mudb_server")%>'
|
||||
var vue = new Vue({
|
||||
el: '#users_table',
|
||||
data: {
|
||||
title: '用户管理',
|
||||
users: []
|
||||
},
|
||||
methods: {
|
||||
clear_log: function() {
|
||||
XHR.get(api_url + "/clear_log", null,
|
||||
function (x, result) {
|
||||
if (x.status == 200) {
|
||||
var log_textarea = document.getElementById('log_textarea');
|
||||
log_textarea.innerHTML = "";
|
||||
log_textarea.scrollTop = log_textarea.scrollHeight;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
check_enable: function(user, e) {
|
||||
if (e.currentTarget.checked == true) {
|
||||
document.getElementById("ssr_mudb_server_" + user.port + ".enable").value = "1";
|
||||
} else {
|
||||
document.getElementById("ssr_mudb_server_" + user.port + ".enable").value = "0";
|
||||
}
|
||||
},
|
||||
get_link: function(user) {
|
||||
XHR.get(api_url + "/get_link", { port : user.port },
|
||||
function(x, result) {
|
||||
if(x && x.status == 200)
|
||||
alert(result.link);
|
||||
}
|
||||
)
|
||||
},
|
||||
clear_transfer: function(user) {
|
||||
if (confirm('确认清空“' + user.user + '”的使用流量吗?') == true) {
|
||||
XHR.get(api_url + "/clear_traffic", { port: user.port },
|
||||
function(x, result) {
|
||||
if(x && x.status == 200)
|
||||
window.location.replace(window.location.href);
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
remove: function(user) {
|
||||
if (confirm('确认删除“' + user.user + '”吗?') == true) {
|
||||
XHR.get(api_url + "/remove_user", { port: user.port },
|
||||
function(x, result) {
|
||||
if(x && x.status == 200)
|
||||
window.location.replace(window.location.href);
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
byte_format: function(byte) {
|
||||
var suff = [ "B", "KB", "MB", "GB", "TB" ];
|
||||
for (var i = 0; i < suff.length; i++) {
|
||||
if (byte > 1024 && i < suff.length) {
|
||||
byte = (byte / 1024).toFixed(2);
|
||||
} else {
|
||||
return byte + " " + suff[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
XHR.get(api_url + "/user_list", null,
|
||||
function (x, result) {
|
||||
if (x.status == 200) {
|
||||
vue.users = result;
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
XHR.poll(2, api_url + "/get_log", null,
|
||||
function (x, result) {
|
||||
if (x.status == 200) {
|
||||
var log_textarea = document.getElementById('log_textarea');
|
||||
log_textarea.innerHTML = x.responseText;
|
||||
log_textarea.scrollTop = log_textarea.scrollHeight;
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
@ -0,0 +1,152 @@
|
||||
msgid "ShadowsocksR MuDB Server"
|
||||
msgstr "ShadowsocksR MuDB 服务器"
|
||||
|
||||
msgid "SSR MuDB Server"
|
||||
msgstr "SSR MuDB 服务器"
|
||||
|
||||
msgid "Global Settings"
|
||||
msgstr "全局设置"
|
||||
|
||||
msgid "Server Config"
|
||||
msgstr "服务器配置"
|
||||
|
||||
msgid "Users Manager"
|
||||
msgstr "用户管理"
|
||||
|
||||
msgid "Remarks"
|
||||
msgstr "备注"
|
||||
|
||||
msgid "Port"
|
||||
msgstr "端口"
|
||||
|
||||
msgid "Password"
|
||||
msgstr "密码"
|
||||
|
||||
msgid "Encrypt Method"
|
||||
msgstr "加密"
|
||||
|
||||
msgid "Protocol"
|
||||
msgstr "协议"
|
||||
|
||||
msgid "Protocol Param"
|
||||
msgstr "协议参数"
|
||||
|
||||
msgid "Obfs"
|
||||
msgstr "混淆"
|
||||
|
||||
msgid "Obfs Param"
|
||||
msgstr "混淆参数"
|
||||
|
||||
msgid "Connection Timeout"
|
||||
msgstr "连接超时时间"
|
||||
|
||||
msgid "redirect"
|
||||
msgstr "重定向"
|
||||
|
||||
msgid "Fast Open"
|
||||
msgstr "快速打开"
|
||||
|
||||
msgid "UDP Forward"
|
||||
msgstr "UDP转发"
|
||||
|
||||
msgid "Null"
|
||||
msgstr "无"
|
||||
|
||||
msgid "No Speed Limit"
|
||||
msgstr "不限速"
|
||||
|
||||
msgid "Forbidden Port"
|
||||
msgstr "禁止的端口"
|
||||
|
||||
msgid "Device Limit"
|
||||
msgstr "设备数限制"
|
||||
|
||||
msgid "Speed Limit Per Con"
|
||||
msgstr "单线程限速"
|
||||
|
||||
msgid "Speed Limit Per User"
|
||||
msgstr "总限速"
|
||||
|
||||
msgid "Available Total Flow"
|
||||
msgstr "可用总流量"
|
||||
|
||||
msgid "Infinite"
|
||||
msgstr "无限"
|
||||
|
||||
msgid "Used Upload Traffic"
|
||||
msgstr "已用上传流量"
|
||||
|
||||
msgid "Used Download Traffic"
|
||||
msgstr "已用下载流量"
|
||||
|
||||
msgid "Used Total Traffic"
|
||||
msgstr "已用总流量"
|
||||
|
||||
msgid "SSR Link"
|
||||
msgstr "SSR链接"
|
||||
|
||||
msgid "GET"
|
||||
msgstr "获取"
|
||||
|
||||
msgid "Clear Traffic"
|
||||
msgstr "清空流量"
|
||||
|
||||
msgid "Clear All Users Traffic"
|
||||
msgstr "清空所有用户流量"
|
||||
|
||||
msgid "Enable Auto Clear Traffic"
|
||||
msgstr "启用自动清空流量"
|
||||
|
||||
msgid "Clear Traffic Time Interval"
|
||||
msgstr "流量清空时间间隔"
|
||||
|
||||
msgid "*,*,*,*,* is Min Hour Day Mon Week"
|
||||
msgstr "*,*,*,*,* 分别对应 分钟 小时 日份 月份 星期<br>0,2,1,*,* 代表 每月1日2点0分<br>0,2,15,*,* 代表 每月15日2点0分<br>0,2,*/7,*,* 代表 每7天2点0分<br>0,2,*,*,0 代表 每个星期日(7)<br>0,2,*,*,3 代表 每个星期三(3)"
|
||||
|
||||
msgid "Number of clients that can be linked at the same time (multi-port mode, each port is calculated independently), a minimum of 2 is recommended."
|
||||
msgstr "同一时间能链接的客户端数量(多端口模式,每个端口都是独立计算),建议最少 2个。"
|
||||
|
||||
msgid "Single thread speed limit upper limit, multithreading is invalid. Zero means no speed limit. (unit: KB/S)"
|
||||
msgstr "单线程的限速上限,多线程即无效。0代表不限速。(单位:KB/S)"
|
||||
|
||||
msgid "Total speed limit upper limit, single port overall speed limit. Zero means no speed limit. (unit: KB/S)"
|
||||
msgstr "总速度限速上限,单个端口整体限速。0代表不限速。(单位:KB/S)"
|
||||
|
||||
msgid "For example, if port 25 is not allowed, the user will not be able to access the mail port 25 through the SSR agent. If 80,443 is disabled, the user will not be able to access the HTTP/HTTPS website normally. <br>blocked single port format: 25<br>blocked multiple port format: 23,465<br>blocked port format: 233-266<br>blocked multiple port format: 25,465,233-666"
|
||||
msgstr "例如不允许访问 25端口,用户就无法通过SSR代理访问邮件端口25了,如果禁止了 80,443 那么用户将无法正常访问 http/https 网站。<br>封禁单个端口格式: 25<br>封禁多个端口格式: 23,465<br>封禁端口段格式: 233-266<br>封禁多种格式端口: 25,465,233-666"
|
||||
|
||||
msgid "Maximum amount of total traffic available (GB, 1-838868), Zero means infinite."
|
||||
msgstr "可使用的总流量上限(单位: GB, 1-838868),0代表无限"
|
||||
|
||||
msgid "Alter ID"
|
||||
msgstr "额外ID(AlterID)"
|
||||
|
||||
msgid "User Level"
|
||||
msgstr "用户等级(Level)"
|
||||
|
||||
msgid "Transport"
|
||||
msgstr "传输方式"
|
||||
|
||||
msgid "Camouflage Type"
|
||||
msgstr "伪装类型"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
msgid "Current Condition"
|
||||
msgstr "当前状态"
|
||||
|
||||
msgid "please Install the python3"
|
||||
msgstr "请安装python3环境"
|
||||
|
||||
msgid "NOT INSTALLED"
|
||||
msgstr "未安装"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
@ -0,0 +1,4 @@
|
||||
|
||||
config global
|
||||
option auto_clear_transfer '0'
|
||||
option enable '0'
|
@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"u": 0,
|
||||
"method": "none",
|
||||
"user": "test",
|
||||
"enable": 1,
|
||||
"forbidden_port": "",
|
||||
"transfer_enable": 900727656415232,
|
||||
"passwd": "123456",
|
||||
"speed_limit_per_user": 0,
|
||||
"port": 50005,
|
||||
"d": 0,
|
||||
"protocol": "auth_chain_a",
|
||||
"obfs": "tls1.2_ticket_auth",
|
||||
"protocol_param": "10",
|
||||
"speed_limit_per_con": 0
|
||||
}
|
||||
]
|
104
package/lienol/luci-app-ssr-mudb-server/root/etc/init.d/ssr_mudb_server
Executable file
104
package/lienol/luci-app-ssr-mudb-server/root/etc/init.d/ssr_mudb_server
Executable file
@ -0,0 +1,104 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2018-2020 Lienol <lawlienol@gmail.com>
|
||||
|
||||
START=99
|
||||
|
||||
CONFIG=ssr_mudb_server
|
||||
ssr_path=/usr/share/$CONFIG
|
||||
|
||||
config_t_get() {
|
||||
local index=0
|
||||
[ -n "$4" ] && index=$4
|
||||
local ret=$(uci -q get $CONFIG.@$1[$index].$2 2>/dev/null)
|
||||
echo ${ret:=$3}
|
||||
}
|
||||
|
||||
add_rule() {
|
||||
iptables -N SSR_MUDB-SERVER
|
||||
iptables -I INPUT -j SSR_MUDB-SERVER
|
||||
lua $ssr_path/firewall.lua
|
||||
}
|
||||
|
||||
del_rule() {
|
||||
count=$(iptables -n -L INPUT 2>/dev/null | grep -c "SSR_MUDB-SERVER")
|
||||
if [ -n "$count" ]; then
|
||||
until [ "$count" = 0 ]
|
||||
do
|
||||
rules=$(iptables -n -L INPUT --line-num 2>/dev/null | grep "SSR_MUDB-SERVER" | awk '{print $1}')
|
||||
for rule in $rules
|
||||
do
|
||||
iptables -D INPUT $rule 2>/dev/null
|
||||
break
|
||||
done
|
||||
count=$(expr $count - 1)
|
||||
done
|
||||
fi
|
||||
iptables -F SSR_MUDB-SERVER 2>/dev/null && iptables -X SSR_MUDB-SERVER 2>/dev/null
|
||||
}
|
||||
|
||||
gen_include() {
|
||||
echo '#!/bin/sh' > /var/etc/$CONFIG.include
|
||||
extract_rules() {
|
||||
echo "*$1"
|
||||
iptables-save -t $1 | grep "SSR_MUDB-SERVER" | \
|
||||
sed -e "s/^-A \(INPUT\)/-I \1 1/"
|
||||
echo 'COMMIT'
|
||||
}
|
||||
cat <<-EOF >> /var/etc/$CONFIG.include
|
||||
iptables-save -c | grep -v "SSR_MUDB-SERVER" | iptables-restore -c
|
||||
iptables-restore -n <<-EOT
|
||||
$(extract_rules filter)
|
||||
EOT
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
set_ssr_python_crontab() {
|
||||
if [ "$1" -eq 1 ];then
|
||||
auto_clear_transfer=$(config_t_get global auto_clear_transfer 0)
|
||||
if [ "$auto_clear_transfer" = "0" ];then
|
||||
sed -i '/clear_traffic_all_users.sh/d' /etc/crontabs/root >/dev/null 2>&1 &
|
||||
else
|
||||
auto_clear_transfer_time=$(config_t_get global auto_clear_transfer_time)
|
||||
[ -n "$auto_clear_transfer_time" ] && auto_clear_transfer_time=$(echo $auto_clear_transfer_time | tr ',' ' ')
|
||||
echo "$auto_clear_transfer_time $ssr_path/clear_traffic_all_users.sh >/dev/null 2>&1" >> /etc/crontabs/root
|
||||
fi
|
||||
else
|
||||
sed -i '/clear_traffic_all_users.sh/d' /etc/crontabs/root >/dev/null 2>&1 &
|
||||
fi
|
||||
/etc/init.d/cron restart
|
||||
}
|
||||
|
||||
start_ssr_python_server() {
|
||||
/usr/bin/python3 $ssr_path/server.py >> /var/log/$CONFIG.log 2>&1 &
|
||||
set_ssr_python_crontab 1
|
||||
add_rule
|
||||
gen_include
|
||||
}
|
||||
|
||||
stop_ssr_python_server() {
|
||||
ps -w | grep "$ssr_path/server.py" | grep -v "grep" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 &
|
||||
set_ssr_python_crontab 0
|
||||
del_rule
|
||||
rm -rf /var/log/$CONFIG.log /var/etc/$CONFIG.include
|
||||
}
|
||||
|
||||
start() {
|
||||
enable=$(config_t_get global enable 0)
|
||||
if [ "$enable" -eq 1 ];then
|
||||
users_config=$(cat $ssr_path/userapiconfig.py | grep "MUDB_FILE" | cut -d "'" -f 2)
|
||||
[ -n "$users_config" ] && cp -rf $users_config $ssr_path/mudb_backup.json
|
||||
start_ssr_python_server
|
||||
else
|
||||
stop_ssr_python_server
|
||||
fi
|
||||
}
|
||||
|
||||
stop() {
|
||||
stop_ssr_python_server
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
start
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete firewall.ssr_mudb_server
|
||||
set firewall.ssr_mudb_server=include
|
||||
set firewall.ssr_mudb_server.type=script
|
||||
set firewall.ssr_mudb_server.path=/var/etc/ssr_mudb_server.include
|
||||
set firewall.ssr_mudb_server.reload=1
|
||||
EOF
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@ssr_mudb_server[-1]
|
||||
add ucitrack ssr_mudb_server
|
||||
set ucitrack.@ssr_mudb_server[-1].init=ssr_mudb_server
|
||||
commit ucitrack
|
||||
EOF
|
||||
|
||||
chmod a+x /usr/share/ssr_mudb_server/* >/dev/null 2>&1
|
||||
cp -rf /usr/bin/python3 /usr/bin/python
|
||||
rm -rf /tmp/luci-*cache
|
||||
exit 0
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"luci-app-ssr-mudb-server": {
|
||||
"description": "Grant UCI access for luci-app-ssr-mudb-server",
|
||||
"read": {
|
||||
"uci": [ "ssr_mudb_server" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "ssr_mudb_server" ]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
language: python3
|
||||
python3:
|
||||
- 2.6
|
||||
- 2.7
|
||||
- 3.3
|
||||
- 3.4
|
||||
cache:
|
||||
directories:
|
||||
- dante-1.4.0
|
||||
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 sh -c "echo '127.0.0.1 localhost' > /etc/hosts"
|
||||
- sudo service nginx restart
|
||||
- pip install pep8 pyflakes nose coverage PySocks cymysql
|
||||
- sudo tests/socksify/install.sh
|
||||
- sudo tests/libsodium/install.sh
|
||||
- sudo tests/setup_tc.sh
|
||||
script:
|
||||
- tests/jenkins.sh
|
@ -0,0 +1,342 @@
|
||||
3.4.0 2017-07-27
|
||||
- add auth_chain_b
|
||||
- add initmudbjson.sh
|
||||
- allow set speed limit in runtime
|
||||
- fix bugs & mem leak
|
||||
|
||||
3.3.3 2017-06-03
|
||||
- add DNS cache
|
||||
- add tls1.2_ticket_fastauth
|
||||
- fix bugs
|
||||
|
||||
3.3.2 2017-05-20
|
||||
- revert http reply
|
||||
- refine tls1.2_ticket_auth error detector
|
||||
|
||||
3.3.1 2017-05-18
|
||||
- fix stop script
|
||||
- Async DNS query under UDP
|
||||
- fix old version of OpenSSL
|
||||
- http reply
|
||||
|
||||
3.3.0 2017-05-11
|
||||
- connect_log include local addr & port
|
||||
- fix auth_chain_a UDP bug
|
||||
- add "additional_ports_only"
|
||||
- add interface legendsockssr
|
||||
- run with newest python3 version
|
||||
- parse comment in hosts
|
||||
- update mujson_mgr
|
||||
- add cymysql setup script
|
||||
- new speed tester
|
||||
- fix leaks
|
||||
- bugs fixed
|
||||
|
||||
3.2.0 2017-04-27
|
||||
- add auth_chain_a
|
||||
- remove auth_aes128, auth_sha1, auth_sha1_v2, verify_simple, auth_simple, verify_sha1
|
||||
|
||||
3.1.2 2017-04-07
|
||||
- display UID
|
||||
- auto adjust TCP MSS
|
||||
|
||||
3.1.1 2017-03-25
|
||||
- add "New session ticket"
|
||||
- ignore bind 10.0.0.0/8 and 192.168.0.0/16 by default
|
||||
- improve rand size under auth_aes128_*
|
||||
- fix bugs
|
||||
|
||||
3.1.0 2017-03-16
|
||||
- add "glzjinmod" interface
|
||||
- rate limit
|
||||
- add additional_ports in config
|
||||
|
||||
3.0.4 2017-01-08
|
||||
- multi-user in single port
|
||||
|
||||
3.0.1 2017-01-03
|
||||
- remove auth_aes128_*_compatible
|
||||
|
||||
3.0.0 2016-12-23
|
||||
- http_simple fix bugs
|
||||
- tls1.2_ticket_auth fix bug & defaule time diff set to 86400s
|
||||
|
||||
2.9.7 2016-11-22
|
||||
- manage client with LRUCache
|
||||
- catch bind error
|
||||
- fix import error of resource on windows
|
||||
- print RLIMIT_NOFILE
|
||||
- always close cymysql objects
|
||||
- add init script
|
||||
|
||||
2.9.6 2016-10-17
|
||||
- tls1.2_ticket_auth random packet size
|
||||
|
||||
2.9.5.1 2016-10-16
|
||||
- UDP bind address
|
||||
|
||||
2.9.5 2016-10-13
|
||||
- add auth_aes128_md5 and auth_aes128_sha1
|
||||
|
||||
2.9.4 2016-10-11
|
||||
- sync client version
|
||||
|
||||
2.6.13 2015-11-02
|
||||
- add protocol setting
|
||||
|
||||
2.6.12 2015-10-27
|
||||
- IPv6 first
|
||||
- Fix mem leaks
|
||||
- auth_simple plugin
|
||||
- remove FORCE_NEW_PROTOCOL
|
||||
- optimize code
|
||||
|
||||
2.6.11 2015-10-20
|
||||
- Obfs plugin
|
||||
- Obfs parameters
|
||||
- UDP over TCP
|
||||
- TCP over UDP (experimental)
|
||||
- Fix socket leaks
|
||||
- Catch abnormal UDP package
|
||||
|
||||
2.6.10 2015-06-08
|
||||
- Optimize LRU cache
|
||||
- Refine logging
|
||||
|
||||
2.6.9 2015-05-19
|
||||
- Fix a stability issue on Windows
|
||||
|
||||
2.6.8 2015-02-10
|
||||
- Support multiple server ip on client side
|
||||
- Support --version
|
||||
- Minor fixes
|
||||
|
||||
2.6.7 2015-02-02
|
||||
- Support --user
|
||||
- Support CIDR format in --forbidden-ip
|
||||
- Minor fixes
|
||||
|
||||
2.6.6 2015-01-23
|
||||
- Fix a crash in forbidden list
|
||||
|
||||
2.6.5 2015-01-18
|
||||
- Try both 32 bit and 64 bit dll on Windows
|
||||
|
||||
2.6.4 2015-01-14
|
||||
- Also search lib* when searching libraries
|
||||
|
||||
2.6.3 2015-01-12
|
||||
- Support --forbidden-ip to ban some IP, i.e. localhost
|
||||
- Search OpenSSL and libsodium harder
|
||||
- Now works on OpenWRT
|
||||
|
||||
2.6.2 2015-01-03
|
||||
- Log client IP
|
||||
|
||||
2.6.1 2014-12-26
|
||||
- Fix a problem with TCP Fast Open on local side
|
||||
- Fix sometimes daemon_start returns wrong exit status
|
||||
|
||||
2.6 2014-12-21
|
||||
- Add daemon support
|
||||
|
||||
2.5 2014-12-11
|
||||
- Add salsa20 and chacha20
|
||||
|
||||
2.4.3 2014-11-10
|
||||
- Fix an issue on Python 3
|
||||
- Fix an issue with IPv6
|
||||
|
||||
2.4.2 2014-11-06
|
||||
- Fix command line arguments on Python 3
|
||||
- Support table on Python 3
|
||||
- Fix TCP Fast Open on Python 3
|
||||
|
||||
2.4.1 2014-11-01
|
||||
- Fix setup.py for non-utf8 locales on Python 3
|
||||
|
||||
2.4 2014-11-01
|
||||
- Python 3 support
|
||||
- Performance improvement
|
||||
- Fix LRU cache behavior
|
||||
|
||||
2.3.2 2014-10-11
|
||||
- Fix OpenSSL on Windows
|
||||
|
||||
2.3.1 2014-10-09
|
||||
- Does not require M2Crypto any more
|
||||
|
||||
2.3 2014-09-23
|
||||
- Support CFB1, CFB8 and CTR mode of AES
|
||||
- Do not require password config when using port_password
|
||||
- Use SIGTERM instead of SIGQUIT on Windows
|
||||
|
||||
2.2.2 2014-09-14
|
||||
- Fix when multiple DNS set, IPv6 only sites are broken
|
||||
|
||||
2.2.1 2014-09-10
|
||||
- Support graceful shutdown
|
||||
- Fix some bugs
|
||||
|
||||
2.2.0 2014-09-09
|
||||
- Add RC4-MD5 encryption
|
||||
|
||||
2.1.0 2014-08-10
|
||||
- Use only IPv4 DNS server
|
||||
- Does not ship config.json
|
||||
- Better error message
|
||||
|
||||
2.0.12 2014-07-26
|
||||
- Support -q quiet mode
|
||||
- Exit 0 when showing help with -h
|
||||
|
||||
2.0.11 2014-07-12
|
||||
- Prefers IP addresses over hostnames, more friendly with socksify and openvpn
|
||||
|
||||
2.0.10 2014-07-11
|
||||
- Fix UDP on local
|
||||
|
||||
2.0.9 2014-07-06
|
||||
- Fix EWOULDBLOCK on Windows
|
||||
- Fix Unicode config problem on some platforms
|
||||
|
||||
2.0.8 2014-06-23
|
||||
- Use multiple DNS to query hostnames
|
||||
|
||||
2.0.7 2014-06-21
|
||||
- Fix fastopen on local
|
||||
- Fallback when fastopen is not available
|
||||
- Add verbose logging mode -vv
|
||||
- Verify if hostname is valid
|
||||
|
||||
2.0.6 2014-06-19
|
||||
- Fix CPU 100% on POLL_HUP
|
||||
- More friendly logging
|
||||
|
||||
2.0.5 2014-06-18
|
||||
- Support a simple config format for multiple ports
|
||||
|
||||
2.0.4 2014-06-12
|
||||
- Fix worker master
|
||||
|
||||
2.0.3 2014-06-11
|
||||
- Fix table encryption with UDP
|
||||
|
||||
2.0.2 2014-06-11
|
||||
- Add asynchronous DNS in TCP relay
|
||||
|
||||
2.0.1 2014-06-05
|
||||
- Better logging
|
||||
- Maybe fix bad file descriptor
|
||||
|
||||
2.0 2014-06-05
|
||||
- Use a new event model
|
||||
- Remove gevent
|
||||
- Refuse to use default password
|
||||
- Fix a problem when using multiple passwords with table encryption
|
||||
|
||||
1.4.5 2014-05-24
|
||||
- Add timeout in TCP server
|
||||
- Close sockets in master process
|
||||
|
||||
1.4.4 2014-05-17
|
||||
- Support multiple workers
|
||||
|
||||
1.4.3 2014-05-13
|
||||
- Fix Windows
|
||||
|
||||
1.4.2 2014-05-10
|
||||
- Add salsa20-ctr cipher
|
||||
|
||||
1.4.1 2014-05-03
|
||||
- Fix error log
|
||||
- Fix EINPROGESS with some version of gevent
|
||||
|
||||
1.4.0 2014-05-02
|
||||
- Adds UDP relay
|
||||
- TCP fast open support on Linux 3.7+
|
||||
|
||||
1.3.7 2014-04-10
|
||||
- Fix a typo in help
|
||||
|
||||
1.3.6 2014-04-10
|
||||
- Fix a typo in help
|
||||
|
||||
1.3.5 2014-04-07
|
||||
- Add help
|
||||
- Change default local binding address into 127.0.0.1
|
||||
|
||||
1.3.4 2014-02-17
|
||||
- Fix a bug when no config file exists
|
||||
- Client now support multiple server ports and multiple server/port pairs
|
||||
- Better error message with bad config.json format and wrong password
|
||||
|
||||
1.3.3 2013-07-09
|
||||
- Fix default key length of rc2
|
||||
|
||||
1.3.2 2013-07-04
|
||||
- Server will listen at server IP specified in config
|
||||
- Check config file and show some warning messages
|
||||
|
||||
1.3.1 2013-06-29
|
||||
- Fix -c arg
|
||||
|
||||
1.3.0 2013-06-22
|
||||
- Move to pypi
|
||||
|
||||
1.2.3 2013-06-14
|
||||
- add bind address
|
||||
|
||||
1.2.2 2013-05-31
|
||||
- local can listen at ::0 with -6 arg; bump 1.2.2
|
||||
|
||||
1.2.1 2013-05-23
|
||||
- Fix an OpenSSL crash
|
||||
|
||||
1.2 2013-05-22
|
||||
- Use random iv, we finally have strong encryption
|
||||
|
||||
1.1.1 2013-05-21
|
||||
- Add encryption, AES, blowfish, etc.
|
||||
|
||||
1.1 2013-05-16
|
||||
- Support IPv6 addresses (type 4)
|
||||
- Drop Python 2.5 support
|
||||
|
||||
1.0 2013-04-03
|
||||
- Fix -6 IPv6
|
||||
|
||||
0.9.4 2013-03-04
|
||||
- Support Python 2.5
|
||||
|
||||
0.9.3 2013-01-14
|
||||
- Fix conn termination null data
|
||||
|
||||
0.9.2 2013-01-05
|
||||
- Change default timeout
|
||||
|
||||
0.9.1 2013-01-05
|
||||
- Add Travis-CI test
|
||||
|
||||
0.9 2012-12-30
|
||||
- Replace send with sendall, fix FreeBSD
|
||||
|
||||
0.6 2012-12-06
|
||||
- Support args
|
||||
|
||||
0.5 2012-11-08
|
||||
- Fix encryption with negative md5sum
|
||||
|
||||
0.4 2012-11-02
|
||||
- Move config into a JSON file
|
||||
- Auto-detect config path
|
||||
|
||||
0.3 2012-06-06
|
||||
- Move socks5 negotiation to local
|
||||
|
||||
0.2 2012-05-11
|
||||
- Add -6 arg for IPv6
|
||||
- Fix socket.error
|
||||
|
||||
0.1 2012-04-20
|
||||
- Initial version
|
@ -0,0 +1,29 @@
|
||||
How to Contribute
|
||||
=================
|
||||
|
||||
Pull Requests
|
||||
-------------
|
||||
|
||||
1. Pull requests are welcome. If you would like to add a large feature
|
||||
or make a significant change, make sure to open an issue to discuss with
|
||||
people first.
|
||||
2. Follow PEP8.
|
||||
3. Make sure to pass the unit tests. Write unit tests for new modules if
|
||||
needed.
|
||||
|
||||
Issues
|
||||
------
|
||||
|
||||
1. Only bugs and feature requests are accepted here.
|
||||
2. We'll only work on important features. If the feature you're asking only
|
||||
benefits a few people, you'd better implement the feature yourself and send us
|
||||
a pull request, or ask some of your friends to do so.
|
||||
3. We don't answer questions of any other types here. Since very few people
|
||||
are watching the issue tracker here, you'll probably get no help from here.
|
||||
Read [Troubleshooting] and get help from forums or [mailing lists].
|
||||
4. Issues in languages other than English will be Google translated into English
|
||||
later.
|
||||
|
||||
|
||||
[Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting
|
||||
[mailing lists]: https://groups.google.com/forum/#!forum/shadowsocks
|
@ -0,0 +1,31 @@
|
||||
FROM alpine:3.6
|
||||
|
||||
ENV SERVER_ADDR 0.0.0.0
|
||||
ENV SERVER_PORT 51348
|
||||
ENV PASSWORD psw
|
||||
ENV METHOD aes-128-ctr
|
||||
ENV PROTOCOL auth_aes128_md5
|
||||
ENV PROTOCOLPARAM 32
|
||||
ENV OBFS tls1.2_ticket_auth_compatible
|
||||
ENV TIMEOUT 300
|
||||
ENV DNS_ADDR 8.8.8.8
|
||||
ENV DNS_ADDR_2 8.8.4.4
|
||||
|
||||
ARG BRANCH=manyuser
|
||||
ARG WORK=~
|
||||
|
||||
|
||||
RUN apk --no-cache add python3 \
|
||||
libsodium \
|
||||
wget
|
||||
|
||||
|
||||
RUN mkdir -p $WORK && \
|
||||
wget -qO- --no-check-certificate https://github.com/shadowsocksr/shadowsocksr/archive/$BRANCH.tar.gz | tar -xzf - -C $WORK
|
||||
|
||||
|
||||
WORKDIR $WORK/shadowsocksr-$BRANCH/shadowsocks
|
||||
|
||||
|
||||
EXPOSE $SERVER_PORT
|
||||
CMD python3 server.py -p $SERVER_PORT -k $PASSWORD -m $METHOD -O $PROTOCOL -o $OBFS -G $PROTOCOLPARAM
|
@ -0,0 +1,3 @@
|
||||
recursive-include shadowsocks *.py
|
||||
include README.rst
|
||||
include LICENSE
|
@ -0,0 +1,15 @@
|
||||
# Config
|
||||
API_INTERFACE = 'sspanelv2' #mudbjson, sspanelv2, sspanelv3, sspanelv3ssr, glzjinmod, legendsockssr, muapiv2(not support)
|
||||
UPDATE_TIME = 60
|
||||
SERVER_PUB_ADDR = '127.0.0.1' # mujson_mgr need this to generate ssr link
|
||||
|
||||
#mudb
|
||||
MUDB_FILE = '/etc/config/ssr_mudb_server.json'
|
||||
|
||||
# Mysql
|
||||
MYSQL_CONFIG = 'usermysql.json'
|
||||
|
||||
# API
|
||||
MUAPI_CONFIG = 'usermuapi.json'
|
||||
|
||||
|
@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import time
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import re
|
||||
import logging
|
||||
from shadowsocks import common
|
||||
from shadowsocks import lru_cache
|
||||
from shadowsocks import eventloop
|
||||
import server_pool
|
||||
import Config
|
||||
|
||||
class ServerMgr(object):
|
||||
|
||||
def __init__(self):
|
||||
self._loop = None
|
||||
self._request_id = 1
|
||||
self._hosts = {}
|
||||
self._hostname_status = {}
|
||||
self._hostname_to_cb = {}
|
||||
self._cb_to_hostname = {}
|
||||
self._last_time = time.time()
|
||||
self._sock = None
|
||||
self._servers = None
|
||||
|
||||
def add_to_loop(self, loop):
|
||||
if self._loop:
|
||||
raise Exception('already add to loop')
|
||||
self._loop = loop
|
||||
# TODO when dns server is IPv6
|
||||
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
|
||||
socket.SOL_UDP)
|
||||
self._sock.bind((Config.MANAGE_BIND_IP, Config.MANAGE_PORT))
|
||||
self._sock.setblocking(False)
|
||||
loop.add(self._sock, eventloop.POLL_IN, self)
|
||||
|
||||
def _handle_data(self, sock):
|
||||
data, addr = sock.recvfrom(128)
|
||||
#manage pwd:port:passwd:action
|
||||
args = data.split(':')
|
||||
if len(args) < 4:
|
||||
return
|
||||
if args[0] == Config.MANAGE_PASS:
|
||||
if args[3] == '0':
|
||||
server_pool.ServerPool.get_instance().cb_del_server(args[1])
|
||||
elif args[3] == '1':
|
||||
server_pool.ServerPool.get_instance().new_server(args[1], args[2])
|
||||
|
||||
def handle_event(self, sock, fd, event):
|
||||
if sock != self._sock:
|
||||
return
|
||||
if event & eventloop.POLL_ERR:
|
||||
logging.error('mgr socket err')
|
||||
self._loop.remove(self._sock)
|
||||
self._sock.close()
|
||||
# TODO when dns server is IPv6
|
||||
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
|
||||
socket.SOL_UDP)
|
||||
self._sock.setblocking(False)
|
||||
self._loop.add(self._sock, eventloop.POLL_IN, self)
|
||||
else:
|
||||
self._handle_data(sock)
|
||||
|
||||
def close(self):
|
||||
if self._sock:
|
||||
if self._loop:
|
||||
self._loop.remove(self._sock)
|
||||
self._sock.close()
|
||||
self._sock = None
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
cd /usr/share/ssr_mudb_server
|
||||
user_total=$(python3 mujson_mgr.py -l | wc -l)
|
||||
[ $user_total -eq 0 ] && echo -e "没有发现用户,请检查 !" && exit 1
|
||||
for i in $(seq 1 $user_total)
|
||||
do
|
||||
port=$(python3 mujson_mgr.py -l | sed -n ${i}p | awk '{print $4}')
|
||||
match_clear=$(python3 mujson_mgr.py -c -p "${port}")
|
||||
done
|
||||
exit
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"server": "0.0.0.0",
|
||||
"server_ipv6": "::",
|
||||
"server_port": 8388,
|
||||
"local_address": "127.0.0.1",
|
||||
"local_port": 1080,
|
||||
|
||||
"password": "m",
|
||||
"method": "aes-128-ctr",
|
||||
"protocol": "auth_aes128_md5",
|
||||
"protocol_param": "",
|
||||
"obfs": "tls1.2_ticket_auth_compatible",
|
||||
"obfs_param": "",
|
||||
"speed_limit_per_con": 0,
|
||||
"speed_limit_per_user": 0,
|
||||
|
||||
"additional_ports" : {}, // only works under multi-user mode
|
||||
"additional_ports_only" : false, // only works under multi-user mode
|
||||
"timeout": 120,
|
||||
"udp_timeout": 60,
|
||||
"dns_ipv6": false,
|
||||
"connect_verbose_info": 0,
|
||||
"redirect": "",
|
||||
"fast_open": false
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
import importloader
|
||||
|
||||
g_config = None
|
||||
|
||||
def load_config():
|
||||
global g_config
|
||||
g_config = importloader.loads(['userapiconfig', 'apiconfig'])
|
||||
|
||||
def get_config():
|
||||
return g_config
|
||||
|
||||
load_config()
|
||||
|
@ -0,0 +1,631 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import logging
|
||||
import time
|
||||
import sys
|
||||
from server_pool import ServerPool
|
||||
import traceback
|
||||
from shadowsocks import common, shell, lru_cache, obfs
|
||||
from configloader import load_config, get_config
|
||||
import importloader
|
||||
|
||||
switchrule = None
|
||||
db_instance = None
|
||||
|
||||
class TransferBase(object):
|
||||
def __init__(self):
|
||||
import threading
|
||||
self.event = threading.Event()
|
||||
self.key_list = ['port', 'u', 'd', 'transfer_enable', 'passwd', 'enable']
|
||||
self.last_get_transfer = {} #上一次的实际流量
|
||||
self.last_update_transfer = {} #上一次更新到的流量(小于等于实际流量)
|
||||
self.force_update_transfer = set() #强制推入数据库的ID
|
||||
self.port_uid_table = {} #端口到uid的映射(仅v3以上有用)
|
||||
self.onlineuser_cache = lru_cache.LRUCache(timeout=60*30) #用户在线状态记录
|
||||
self.pull_ok = False #记录是否已经拉出过数据
|
||||
self.mu_ports = {}
|
||||
|
||||
def load_cfg(self):
|
||||
pass
|
||||
|
||||
def push_db_all_user(self):
|
||||
if self.pull_ok is False:
|
||||
return
|
||||
#更新用户流量到数据库
|
||||
last_transfer = self.last_update_transfer
|
||||
curr_transfer = ServerPool.get_instance().get_servers_transfer()
|
||||
#上次和本次的增量
|
||||
dt_transfer = {}
|
||||
for id in self.force_update_transfer: #此表中的用户统计上次未计入的流量
|
||||
if id in self.last_get_transfer and id in last_transfer:
|
||||
dt_transfer[id] = [self.last_get_transfer[id][0] - last_transfer[id][0], self.last_get_transfer[id][1] - last_transfer[id][1]]
|
||||
|
||||
for id in curr_transfer.keys():
|
||||
if id in self.force_update_transfer or id in self.mu_ports:
|
||||
continue
|
||||
#算出与上次记录的流量差值,保存于dt_transfer表
|
||||
if id in last_transfer:
|
||||
if curr_transfer[id][0] + curr_transfer[id][1] - last_transfer[id][0] - last_transfer[id][1] <= 0:
|
||||
continue
|
||||
dt_transfer[id] = [curr_transfer[id][0] - last_transfer[id][0],
|
||||
curr_transfer[id][1] - last_transfer[id][1]]
|
||||
else:
|
||||
if curr_transfer[id][0] + curr_transfer[id][1] <= 0:
|
||||
continue
|
||||
dt_transfer[id] = [curr_transfer[id][0], curr_transfer[id][1]]
|
||||
|
||||
#有流量的,先记录在线状态
|
||||
if id in self.last_get_transfer:
|
||||
if curr_transfer[id][0] + curr_transfer[id][1] > self.last_get_transfer[id][0] + self.last_get_transfer[id][1]:
|
||||
self.onlineuser_cache[id] = curr_transfer[id][0] + curr_transfer[id][1]
|
||||
else:
|
||||
self.onlineuser_cache[id] = curr_transfer[id][0] + curr_transfer[id][1]
|
||||
|
||||
self.onlineuser_cache.sweep()
|
||||
|
||||
update_transfer = self.update_all_user(dt_transfer) #返回有更新的表
|
||||
for id in update_transfer.keys(): #其增量加在此表
|
||||
if id not in self.force_update_transfer: #但排除在force_update_transfer内的
|
||||
last = self.last_update_transfer.get(id, [0,0])
|
||||
self.last_update_transfer[id] = [last[0] + update_transfer[id][0], last[1] + update_transfer[id][1]]
|
||||
self.last_get_transfer = curr_transfer
|
||||
for id in self.force_update_transfer:
|
||||
if id in self.last_update_transfer:
|
||||
del self.last_update_transfer[id]
|
||||
if id in self.last_get_transfer:
|
||||
del self.last_get_transfer[id]
|
||||
self.force_update_transfer = set()
|
||||
|
||||
def del_server_out_of_bound_safe(self, last_rows, rows):
|
||||
#停止超流量的服务
|
||||
#启动没超流量的服务
|
||||
try:
|
||||
switchrule = importloader.load('switchrule')
|
||||
except Exception as e:
|
||||
logging.error('load switchrule.py fail')
|
||||
cur_servers = {}
|
||||
new_servers = {}
|
||||
allow_users = {}
|
||||
mu_servers = {}
|
||||
config = shell.get_config(False)
|
||||
for row in rows:
|
||||
try:
|
||||
allow = switchrule.isTurnOn(row) and row['enable'] == 1 and row['u'] + row['d'] < row['transfer_enable']
|
||||
except Exception as e:
|
||||
allow = False
|
||||
|
||||
port = row['port']
|
||||
passwd = common.to_bytes(row['passwd'])
|
||||
if hasattr(passwd, 'encode'):
|
||||
passwd = passwd.encode('utf-8')
|
||||
cfg = {'password': passwd}
|
||||
if 'id' in row:
|
||||
self.port_uid_table[row['port']] = row['id']
|
||||
|
||||
read_config_keys = ['method', 'obfs', 'obfs_param', 'protocol', 'protocol_param', 'forbidden_ip', 'forbidden_port', 'speed_limit_per_con', 'speed_limit_per_user']
|
||||
for name in read_config_keys:
|
||||
if name in row and row[name]:
|
||||
cfg[name] = row[name]
|
||||
|
||||
merge_config_keys = ['password'] + read_config_keys
|
||||
for name in cfg.keys():
|
||||
if hasattr(cfg[name], 'encode'):
|
||||
try:
|
||||
cfg[name] = cfg[name].encode('utf-8')
|
||||
except Exception as e:
|
||||
logging.warning('encode cfg key "%s" fail, val "%s"' % (name, cfg[name]))
|
||||
|
||||
if port not in cur_servers:
|
||||
cur_servers[port] = passwd
|
||||
else:
|
||||
logging.error('more than one user use the same port [%s]' % (port,))
|
||||
continue
|
||||
|
||||
if 'protocol' in cfg and 'protocol_param' in cfg and common.to_str(cfg['protocol']) in obfs.mu_protocol():
|
||||
if '#' in common.to_str(cfg['protocol_param']):
|
||||
mu_servers[port] = passwd
|
||||
allow = True
|
||||
|
||||
if allow:
|
||||
if port not in mu_servers:
|
||||
allow_users[port] = cfg
|
||||
|
||||
cfgchange = False
|
||||
if port in ServerPool.get_instance().tcp_servers_pool:
|
||||
relay = ServerPool.get_instance().tcp_servers_pool[port]
|
||||
for name in merge_config_keys:
|
||||
if name in cfg and not self.cmp(cfg[name], relay._config[name]):
|
||||
cfgchange = True
|
||||
break
|
||||
if not cfgchange and port in ServerPool.get_instance().tcp_ipv6_servers_pool:
|
||||
relay = ServerPool.get_instance().tcp_ipv6_servers_pool[port]
|
||||
for name in merge_config_keys:
|
||||
if (name in cfg) and ((name not in relay._config) or not self.cmp(cfg[name], relay._config[name])):
|
||||
cfgchange = True
|
||||
break
|
||||
|
||||
if port in mu_servers:
|
||||
if ServerPool.get_instance().server_is_run(port) > 0:
|
||||
if cfgchange:
|
||||
logging.info('db stop server at port [%s] reason: config changed: %s' % (port, cfg))
|
||||
ServerPool.get_instance().cb_del_server(port)
|
||||
self.force_update_transfer.add(port)
|
||||
new_servers[port] = (passwd, cfg)
|
||||
else:
|
||||
self.new_server(port, passwd, cfg)
|
||||
else:
|
||||
if ServerPool.get_instance().server_is_run(port) > 0:
|
||||
if config['additional_ports_only'] or not allow:
|
||||
logging.info('db stop server at port [%s]' % (port,))
|
||||
ServerPool.get_instance().cb_del_server(port)
|
||||
self.force_update_transfer.add(port)
|
||||
else:
|
||||
if cfgchange:
|
||||
logging.info('db stop server at port [%s] reason: config changed: %s' % (port, cfg))
|
||||
ServerPool.get_instance().cb_del_server(port)
|
||||
self.force_update_transfer.add(port)
|
||||
new_servers[port] = (passwd, cfg)
|
||||
|
||||
elif not config['additional_ports_only'] and allow and port > 0 and port < 65536 and ServerPool.get_instance().server_run_status(port) is False:
|
||||
self.new_server(port, passwd, cfg)
|
||||
|
||||
for row in last_rows:
|
||||
if row['port'] in cur_servers:
|
||||
pass
|
||||
else:
|
||||
logging.info('db stop server at port [%s] reason: port not exist' % (row['port']))
|
||||
ServerPool.get_instance().cb_del_server(row['port'])
|
||||
self.clear_cache(row['port'])
|
||||
if row['port'] in self.port_uid_table:
|
||||
del self.port_uid_table[row['port']]
|
||||
|
||||
if len(new_servers) > 0:
|
||||
from shadowsocks import eventloop
|
||||
self.event.wait(eventloop.TIMEOUT_PRECISION + eventloop.TIMEOUT_PRECISION / 2)
|
||||
for port in new_servers.keys():
|
||||
passwd, cfg = new_servers[port]
|
||||
self.new_server(port, passwd, cfg)
|
||||
|
||||
logging.debug('db allow users %s \nmu_servers %s' % (allow_users, mu_servers))
|
||||
for port in mu_servers:
|
||||
ServerPool.get_instance().update_mu_users(port, allow_users)
|
||||
|
||||
self.mu_ports = mu_servers
|
||||
|
||||
def clear_cache(self, port):
|
||||
if port in self.force_update_transfer: del self.force_update_transfer[port]
|
||||
if port in self.last_get_transfer: del self.last_get_transfer[port]
|
||||
if port in self.last_update_transfer: del self.last_update_transfer[port]
|
||||
|
||||
def new_server(self, port, passwd, cfg):
|
||||
protocol = cfg.get('protocol', ServerPool.get_instance().config.get('protocol', 'origin'))
|
||||
method = cfg.get('method', ServerPool.get_instance().config.get('method', 'None'))
|
||||
obfs = cfg.get('obfs', ServerPool.get_instance().config.get('obfs', 'plain'))
|
||||
logging.info('db start server at port [%s] pass [%s] protocol [%s] method [%s] obfs [%s]' % (port, passwd, protocol, method, obfs))
|
||||
ServerPool.get_instance().new_server(port, cfg)
|
||||
|
||||
def cmp(self, val1, val2):
|
||||
if type(val1) is bytes:
|
||||
val1 = common.to_str(val1)
|
||||
if type(val2) is bytes:
|
||||
val2 = common.to_str(val2)
|
||||
return val1 == val2
|
||||
|
||||
@staticmethod
|
||||
def del_servers():
|
||||
for port in [v for v in ServerPool.get_instance().tcp_servers_pool.keys()]:
|
||||
if ServerPool.get_instance().server_is_run(port) > 0:
|
||||
ServerPool.get_instance().cb_del_server(port)
|
||||
for port in [v for v in ServerPool.get_instance().tcp_ipv6_servers_pool.keys()]:
|
||||
if ServerPool.get_instance().server_is_run(port) > 0:
|
||||
ServerPool.get_instance().cb_del_server(port)
|
||||
|
||||
@staticmethod
|
||||
def thread_db(obj):
|
||||
import socket
|
||||
import time
|
||||
global db_instance
|
||||
timeout = 60
|
||||
socket.setdefaulttimeout(timeout)
|
||||
last_rows = []
|
||||
db_instance = obj()
|
||||
ServerPool.get_instance()
|
||||
shell.log_shadowsocks_version()
|
||||
|
||||
try:
|
||||
import resource
|
||||
logging.info('current process RLIMIT_NOFILE resource: soft %d hard %d' % resource.getrlimit(resource.RLIMIT_NOFILE))
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
while True:
|
||||
load_config()
|
||||
db_instance.load_cfg()
|
||||
try:
|
||||
db_instance.push_db_all_user()
|
||||
rows = db_instance.pull_db_all_user()
|
||||
if rows:
|
||||
db_instance.pull_ok = True
|
||||
config = shell.get_config(False)
|
||||
for port in config['additional_ports']:
|
||||
val = config['additional_ports'][port]
|
||||
val['port'] = int(port)
|
||||
val['enable'] = 1
|
||||
val['transfer_enable'] = 1024 ** 7
|
||||
val['u'] = 0
|
||||
val['d'] = 0
|
||||
if "password" in val:
|
||||
val["passwd"] = val["password"]
|
||||
rows.append(val)
|
||||
db_instance.del_server_out_of_bound_safe(last_rows, rows)
|
||||
last_rows = rows
|
||||
except Exception as e:
|
||||
trace = traceback.format_exc()
|
||||
logging.error(trace)
|
||||
#logging.warn('db thread except:%s' % e)
|
||||
if db_instance.event.wait(get_config().UPDATE_TIME) or not ServerPool.get_instance().thread.is_alive():
|
||||
break
|
||||
except KeyboardInterrupt as e:
|
||||
pass
|
||||
db_instance.del_servers()
|
||||
ServerPool.get_instance().stop()
|
||||
db_instance = None
|
||||
|
||||
@staticmethod
|
||||
def thread_db_stop():
|
||||
global db_instance
|
||||
db_instance.event.set()
|
||||
|
||||
class DbTransfer(TransferBase):
|
||||
def __init__(self):
|
||||
super(DbTransfer, self).__init__()
|
||||
self.user_pass = {} #记录更新此用户流量时被跳过多少次
|
||||
self.cfg = {
|
||||
"host": "127.0.0.1",
|
||||
"port": 3306,
|
||||
"user": "ss",
|
||||
"password": "pass",
|
||||
"db": "shadowsocks",
|
||||
"node_id": 0,
|
||||
"transfer_mul": 1.0,
|
||||
"ssl_enable": 0,
|
||||
"ssl_ca": "",
|
||||
"ssl_cert": "",
|
||||
"ssl_key": ""}
|
||||
self.load_cfg()
|
||||
|
||||
def load_cfg(self):
|
||||
import json
|
||||
config_path = get_config().MYSQL_CONFIG
|
||||
cfg = None
|
||||
with open(config_path, 'rb+') as f:
|
||||
cfg = json.loads(f.read().decode('utf8'))
|
||||
|
||||
if cfg:
|
||||
self.cfg.update(cfg)
|
||||
|
||||
def update_all_user(self, dt_transfer):
|
||||
import cymysql
|
||||
update_transfer = {}
|
||||
|
||||
query_head = 'UPDATE user'
|
||||
query_sub_when = ''
|
||||
query_sub_when2 = ''
|
||||
query_sub_in = None
|
||||
last_time = time.time()
|
||||
|
||||
for id in dt_transfer.keys():
|
||||
transfer = dt_transfer[id]
|
||||
#小于最低更新流量的先不更新
|
||||
update_trs = 1024 * (2048 - self.user_pass.get(id, 0) * 64)
|
||||
if transfer[0] + transfer[1] < update_trs and id not in self.force_update_transfer:
|
||||
self.user_pass[id] = self.user_pass.get(id, 0) + 1
|
||||
continue
|
||||
if id in self.user_pass:
|
||||
del self.user_pass[id]
|
||||
|
||||
query_sub_when += ' WHEN %s THEN u+%s' % (id, int(transfer[0] * self.cfg["transfer_mul"]))
|
||||
query_sub_when2 += ' WHEN %s THEN d+%s' % (id, int(transfer[1] * self.cfg["transfer_mul"]))
|
||||
update_transfer[id] = transfer
|
||||
|
||||
if query_sub_in is not None:
|
||||
query_sub_in += ',%s' % id
|
||||
else:
|
||||
query_sub_in = '%s' % id
|
||||
|
||||
if query_sub_when == '':
|
||||
return update_transfer
|
||||
query_sql = query_head + ' SET u = CASE port' + query_sub_when + \
|
||||
' END, d = CASE port' + query_sub_when2 + \
|
||||
' END, t = ' + str(int(last_time)) + \
|
||||
' WHERE port IN (%s)' % query_sub_in
|
||||
if self.cfg["ssl_enable"] == 1:
|
||||
conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"],
|
||||
user=self.cfg["user"], passwd=self.cfg["password"],
|
||||
db=self.cfg["db"], charset='utf8',
|
||||
ssl={'ca':self.cfg["ssl_ca"],'cert':self.cfg["ssl_cert"],'key':self.cfg["ssl_key"]})
|
||||
else:
|
||||
conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"],
|
||||
user=self.cfg["user"], passwd=self.cfg["password"],
|
||||
db=self.cfg["db"], charset='utf8')
|
||||
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute(query_sql)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
update_transfer = {}
|
||||
|
||||
cur.close()
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
update_transfer = {}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return update_transfer
|
||||
|
||||
def pull_db_all_user(self):
|
||||
import cymysql
|
||||
#数据库所有用户信息
|
||||
if self.cfg["ssl_enable"] == 1:
|
||||
conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"],
|
||||
user=self.cfg["user"], passwd=self.cfg["password"],
|
||||
db=self.cfg["db"], charset='utf8',
|
||||
ssl={'ca':self.cfg["ssl_ca"],'cert':self.cfg["ssl_cert"],'key':self.cfg["ssl_key"]})
|
||||
else:
|
||||
conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"],
|
||||
user=self.cfg["user"], passwd=self.cfg["password"],
|
||||
db=self.cfg["db"], charset='utf8')
|
||||
|
||||
try:
|
||||
rows = self.pull_db_users(conn)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if not rows:
|
||||
logging.warn('no user in db')
|
||||
return rows
|
||||
|
||||
def pull_db_users(self, conn):
|
||||
try:
|
||||
switchrule = importloader.load('switchrule')
|
||||
keys = switchrule.getKeys(self.key_list)
|
||||
except Exception as e:
|
||||
keys = self.key_list
|
||||
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT " + ','.join(keys) + " FROM user")
|
||||
rows = []
|
||||
for r in cur.fetchall():
|
||||
d = {}
|
||||
for column in range(len(keys)):
|
||||
d[keys[column]] = r[column]
|
||||
rows.append(d)
|
||||
cur.close()
|
||||
return rows
|
||||
|
||||
class Dbv3Transfer(DbTransfer):
|
||||
def __init__(self):
|
||||
super(Dbv3Transfer, self).__init__()
|
||||
self.update_node_state = True if get_config().API_INTERFACE != 'legendsockssr' else False
|
||||
if self.update_node_state:
|
||||
self.key_list += ['id']
|
||||
self.key_list += ['method']
|
||||
if self.update_node_state:
|
||||
self.ss_node_info_name = 'ss_node_info_log'
|
||||
if get_config().API_INTERFACE == 'sspanelv3ssr':
|
||||
self.key_list += ['obfs', 'protocol']
|
||||
if get_config().API_INTERFACE == 'glzjinmod':
|
||||
self.key_list += ['obfs', 'protocol']
|
||||
self.ss_node_info_name = 'ss_node_info'
|
||||
else:
|
||||
self.key_list += ['obfs', 'protocol']
|
||||
self.start_time = time.time()
|
||||
|
||||
def update_all_user(self, dt_transfer):
|
||||
import cymysql
|
||||
update_transfer = {}
|
||||
|
||||
query_head = 'UPDATE user'
|
||||
query_sub_when = ''
|
||||
query_sub_when2 = ''
|
||||
query_sub_in = None
|
||||
last_time = time.time()
|
||||
|
||||
alive_user_count = len(self.onlineuser_cache)
|
||||
bandwidth_thistime = 0
|
||||
|
||||
if self.cfg["ssl_enable"] == 1:
|
||||
conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"],
|
||||
user=self.cfg["user"], passwd=self.cfg["password"],
|
||||
db=self.cfg["db"], charset='utf8',
|
||||
ssl={'ca':self.cfg["ssl_ca"],'cert':self.cfg["ssl_cert"],'key':self.cfg["ssl_key"]})
|
||||
else:
|
||||
conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"],
|
||||
user=self.cfg["user"], passwd=self.cfg["password"],
|
||||
db=self.cfg["db"], charset='utf8')
|
||||
conn.autocommit(True)
|
||||
|
||||
for id in dt_transfer.keys():
|
||||
transfer = dt_transfer[id]
|
||||
bandwidth_thistime = bandwidth_thistime + transfer[0] + transfer[1]
|
||||
|
||||
update_trs = 1024 * (2048 - self.user_pass.get(id, 0) * 64)
|
||||
if transfer[0] + transfer[1] < update_trs:
|
||||
self.user_pass[id] = self.user_pass.get(id, 0) + 1
|
||||
continue
|
||||
if id in self.user_pass:
|
||||
del self.user_pass[id]
|
||||
|
||||
query_sub_when += ' WHEN %s THEN u+%s' % (id, int(transfer[0] * self.cfg["transfer_mul"]))
|
||||
query_sub_when2 += ' WHEN %s THEN d+%s' % (id, int(transfer[1] * self.cfg["transfer_mul"]))
|
||||
update_transfer[id] = transfer
|
||||
|
||||
if self.update_node_state:
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
if id in self.port_uid_table:
|
||||
cur.execute("INSERT INTO `user_traffic_log` (`id`, `user_id`, `u`, `d`, `node_id`, `rate`, `traffic`, `log_time`) VALUES (NULL, '" + \
|
||||
str(self.port_uid_table[id]) + "', '" + str(transfer[0]) + "', '" + str(transfer[1]) + "', '" + \
|
||||
str(self.cfg["node_id"]) + "', '" + str(self.cfg["transfer_mul"]) + "', '" + \
|
||||
self.traffic_format((transfer[0] + transfer[1]) * self.cfg["transfer_mul"]) + "', unix_timestamp()); ")
|
||||
except:
|
||||
logging.warn('no `user_traffic_log` in db')
|
||||
cur.close()
|
||||
|
||||
if query_sub_in is not None:
|
||||
query_sub_in += ',%s' % id
|
||||
else:
|
||||
query_sub_in = '%s' % id
|
||||
|
||||
if query_sub_when != '':
|
||||
query_sql = query_head + ' SET u = CASE port' + query_sub_when + \
|
||||
' END, d = CASE port' + query_sub_when2 + \
|
||||
' END, t = ' + str(int(last_time)) + \
|
||||
' WHERE port IN (%s)' % query_sub_in
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute(query_sql)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
cur.close()
|
||||
|
||||
if self.update_node_state:
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("INSERT INTO `ss_node_online_log` (`id`, `node_id`, `online_user`, `log_time`) VALUES (NULL, '" + \
|
||||
str(self.cfg["node_id"]) + "', '" + str(alive_user_count) + "', unix_timestamp()); ")
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
cur.close()
|
||||
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("INSERT INTO `" + self.ss_node_info_name + "` (`id`, `node_id`, `uptime`, `load`, `log_time`) VALUES (NULL, '" + \
|
||||
str(self.cfg["node_id"]) + "', '" + str(self.uptime()) + "', '" + \
|
||||
str(self.load()) + "', unix_timestamp()); ")
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
cur.close()
|
||||
except:
|
||||
logging.warn('no `ss_node_online_log` or `" + self.ss_node_info_name + "` in db')
|
||||
|
||||
conn.close()
|
||||
return update_transfer
|
||||
|
||||
def pull_db_users(self, conn):
|
||||
try:
|
||||
switchrule = importloader.load('switchrule')
|
||||
keys = switchrule.getKeys(self.key_list)
|
||||
except Exception as e:
|
||||
keys = self.key_list
|
||||
|
||||
cur = conn.cursor()
|
||||
|
||||
if self.update_node_state:
|
||||
node_info_keys = ['traffic_rate']
|
||||
try:
|
||||
cur.execute("SELECT " + ','.join(node_info_keys) +" FROM ss_node where `id`='" + str(self.cfg["node_id"]) + "'")
|
||||
nodeinfo = cur.fetchone()
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
nodeinfo = None
|
||||
|
||||
if nodeinfo == None:
|
||||
rows = []
|
||||
cur.close()
|
||||
conn.commit()
|
||||
logging.warn('None result when select node info from ss_node in db, maybe you set the incorrect node id')
|
||||
return rows
|
||||
cur.close()
|
||||
|
||||
node_info_dict = {}
|
||||
for column in range(len(nodeinfo)):
|
||||
node_info_dict[node_info_keys[column]] = nodeinfo[column]
|
||||
self.cfg['transfer_mul'] = float(node_info_dict['traffic_rate'])
|
||||
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
rows = []
|
||||
cur.execute("SELECT " + ','.join(keys) + " FROM user")
|
||||
for r in cur.fetchall():
|
||||
d = {}
|
||||
for column in range(len(keys)):
|
||||
d[keys[column]] = r[column]
|
||||
rows.append(d)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
cur.close()
|
||||
return rows
|
||||
|
||||
def load(self):
|
||||
import os
|
||||
return os.popen("cat /proc/loadavg | awk '{ print $1\" \"$2\" \"$3 }'").readlines()[0]
|
||||
|
||||
def uptime(self):
|
||||
return time.time() - self.start_time
|
||||
|
||||
def traffic_format(self, traffic):
|
||||
if traffic < 1024 * 8:
|
||||
return str(int(traffic)) + "B";
|
||||
|
||||
if traffic < 1024 * 1024 * 2:
|
||||
return str(round((traffic / 1024.0), 2)) + "KB";
|
||||
|
||||
return str(round((traffic / 1048576.0), 2)) + "MB";
|
||||
|
||||
class MuJsonTransfer(TransferBase):
|
||||
def __init__(self):
|
||||
super(MuJsonTransfer, self).__init__()
|
||||
|
||||
def update_all_user(self, dt_transfer):
|
||||
import json
|
||||
rows = None
|
||||
|
||||
config_path = get_config().MUDB_FILE
|
||||
with open(config_path, 'rb+') as f:
|
||||
rows = json.loads(f.read().decode('utf8'))
|
||||
for row in rows:
|
||||
if "port" in row:
|
||||
port = row["port"]
|
||||
if port in dt_transfer:
|
||||
row["u"] += dt_transfer[port][0]
|
||||
row["d"] += dt_transfer[port][1]
|
||||
|
||||
if rows:
|
||||
output = json.dumps(rows, sort_keys=True, indent=4, separators=(',', ': '))
|
||||
with open(config_path, 'r+') as f:
|
||||
f.write(output)
|
||||
f.truncate()
|
||||
|
||||
return dt_transfer
|
||||
|
||||
def pull_db_all_user(self):
|
||||
import json
|
||||
rows = None
|
||||
|
||||
config_path = get_config().MUDB_FILE
|
||||
with open(config_path, 'rb+') as f:
|
||||
rows = json.loads(f.read().decode('utf8'))
|
||||
for row in rows:
|
||||
try:
|
||||
if 'forbidden_ip' in row:
|
||||
row['forbidden_ip'] = common.IPNetwork(row['forbidden_ip'])
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
try:
|
||||
if 'forbidden_port' in row:
|
||||
row['forbidden_port'] = common.PortRange(row['forbidden_port'])
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
if not rows:
|
||||
logging.warn('no user in json file')
|
||||
return rows
|
||||
|
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
require 'luci.sys'
|
||||
local jsonc = require "luci.jsonc"
|
||||
|
||||
local function get_config_path()
|
||||
return luci.sys.exec("echo -n $(cat /usr/share/ssr_mudb_server/userapiconfig.py | grep 'MUDB_FILE' | cut -d \"'\" -f 2)")
|
||||
end
|
||||
|
||||
local function get_config_json()
|
||||
return luci.sys.exec("cat " .. get_config_path()) or "[]"
|
||||
end
|
||||
|
||||
local json = jsonc.parse(get_config_json())
|
||||
if json then
|
||||
for index = 1, table.maxn(json) do
|
||||
local o = json[index]
|
||||
if o.enable == 1 then
|
||||
luci.sys.call(string.format("iptables -A SSR_MUDB-SERVER -p tcp --dport %s -m comment --comment %s -j ACCEPT", o.port, o.user))
|
||||
luci.sys.call(string.format("iptables -A SSR_MUDB-SERVER -p udp --dport %s -m comment --comment %s -j ACCEPT", o.port, o.user))
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
def load(name):
|
||||
try:
|
||||
obj = __import__(name)
|
||||
reload(obj)
|
||||
return obj
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
import importlib
|
||||
obj = importlib.__import__(name)
|
||||
importlib.reload(obj)
|
||||
return obj
|
||||
except:
|
||||
pass
|
||||
|
||||
def loads(namelist):
|
||||
for name in namelist:
|
||||
obj = load(name)
|
||||
if obj is not None:
|
||||
return obj
|
@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
If Not Exist "userapiconfig.py" Copy "apiconfig.py" "userapiconfig.py"
|
||||
If Not Exist "user-config.json" Copy "config.json" "user-config.json"
|
||||
If Not Exist "usermysql.json" Copy "mysql.json" "usermysql.json"
|
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
chmod +x *.sh
|
||||
chmod +x shadowsocks/*.sh
|
||||
cp -n apiconfig.py userapiconfig.py
|
||||
cp -n config.json user-config.json
|
||||
cp -n mysql.json usermysql.json
|
||||
|
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
bash initcfg.sh
|
||||
sed -i "s/API_INTERFACE = .\+\?\#/API_INTERFACE = \'mudbjson\' \#/g" userapiconfig.py
|
||||
ip_addr=`ifconfig -a|grep inet|grep -v inet6|grep -v "127.0.0."|grep -v -e "192\.168\..[0-9]\+\.[0-9]\+"|grep -v -e "10\.[0-9]\+\.[0-9]\+\.[0-9]\+"|awk '{print $2}'|tr -d "addr:"`
|
||||
ip_count=`echo $ip_addr|grep -e "^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$" -c`
|
||||
|
||||
if [[ $ip_count == 1 ]]; then
|
||||
ip_addr=`ip a|grep inet|grep -v inet6|grep -v "127.0.0."|grep -v -e "192\.168\..[0-9]\+\.[0-9]\+"|grep -v -e "10\.[0-9]\+\.[0-9]\+\.[0-9]\+"|awk '{print $2}'`
|
||||
ip_addr=${ip_addr%/*}
|
||||
ip_count=`echo $ip_addr|grep -e "^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$" -c`
|
||||
fi
|
||||
if [[ $ip_count == 1 ]]; then
|
||||
echo "server IP is "${ip_addr}
|
||||
sed -i "s/SERVER_PUB_ADDR = .\+/SERVER_PUB_ADDR = \'"${ip_addr}"\'/g" userapiconfig.py
|
||||
user_count=`python3 mujson_mgr.py -l|grep -c -e "[0-9]"`
|
||||
if [[ $user_count == 0 ]]; then
|
||||
port=`python3 -c 'import random;print(random.randint(10000, 65536))'`
|
||||
python3 mujson_mgr.py -a -p ${port}
|
||||
fi
|
||||
else
|
||||
echo "unable to detect server IP"
|
||||
fi
|
||||
|
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
cd `dirname $0`
|
||||
#python3_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1)
|
||||
eval $(ps -ef | grep "[0-9] python3 server\\.py m" | awk '{print "kill "$2}')
|
||||
ulimit -n 512000
|
||||
nohup python3 server.py m >> ssserver.log 2>&1 &
|
||||
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1,358 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import traceback
|
||||
from shadowsocks import shell, common
|
||||
from configloader import load_config, get_config
|
||||
import random
|
||||
import getopt
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
|
||||
|
||||
class MuJsonLoader(object):
|
||||
def __init__(self):
|
||||
self.json = None
|
||||
|
||||
def load(self, path):
|
||||
l = "[]"
|
||||
try:
|
||||
with open(path, 'rb+') as f:
|
||||
l = f.read().decode('utf8')
|
||||
except:
|
||||
pass
|
||||
self.json = json.loads(l)
|
||||
|
||||
def save(self, path):
|
||||
if self.json is not None:
|
||||
output = json.dumps(self.json, sort_keys=True, indent=4, separators=(',', ': '))
|
||||
with open(path, 'a'):
|
||||
pass
|
||||
with open(path, 'rb+') as f:
|
||||
f.write(output.encode('utf8'))
|
||||
f.truncate()
|
||||
|
||||
|
||||
class MuMgr(object):
|
||||
def __init__(self):
|
||||
self.config_path = get_config().MUDB_FILE
|
||||
try:
|
||||
self.server_addr = get_config().SERVER_PUB_ADDR
|
||||
except:
|
||||
self.server_addr = '127.0.0.1'
|
||||
self.data = MuJsonLoader()
|
||||
|
||||
if self.server_addr == '127.0.0.1':
|
||||
self.server_addr = self.getipaddr()
|
||||
|
||||
def getipaddr(self, ifname='eth0'):
|
||||
import socket
|
||||
import struct
|
||||
ret = '127.0.0.1'
|
||||
try:
|
||||
ret = socket.gethostbyname(socket.getfqdn(socket.gethostname()))
|
||||
except:
|
||||
pass
|
||||
if ret == '127.0.0.1':
|
||||
try:
|
||||
import fcntl
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
ret = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))[20:24])
|
||||
except:
|
||||
pass
|
||||
return ret
|
||||
|
||||
def ssrlink(self, user, encode, muid):
|
||||
protocol = user.get('protocol', '')
|
||||
obfs = user.get('obfs', '')
|
||||
protocol = protocol.replace("_compatible", "")
|
||||
obfs = obfs.replace("_compatible", "")
|
||||
protocol_param = ''
|
||||
if muid is not None:
|
||||
protocol_param_ = user.get('protocol_param', '')
|
||||
param = protocol_param_.split('#')
|
||||
if len(param) == 2:
|
||||
for row in self.data.json:
|
||||
if int(row['port']) == muid:
|
||||
param = str(muid) + ':' + row['passwd']
|
||||
protocol_param = '/?protoparam=' + common.to_str(base64.urlsafe_b64encode(common.to_bytes(param))).replace("=", "")
|
||||
break
|
||||
link = ("%s:%s:%s:%s:%s:%s" % (self.server_addr, user['port'], protocol, user['method'], obfs, common.to_str(base64.urlsafe_b64encode(common.to_bytes(user['passwd']))).replace("=", ""))) + protocol_param
|
||||
return "ssr://" + (encode and common.to_str(base64.urlsafe_b64encode(common.to_bytes(link))).replace("=", "") or link)
|
||||
|
||||
def userinfo(self, user, muid = None):
|
||||
ret = ""
|
||||
key_list = ['user', 'port', 'method', 'passwd', 'protocol', 'protocol_param', 'obfs', 'obfs_param', 'transfer_enable', 'u', 'd']
|
||||
for key in sorted(user):
|
||||
if key not in key_list:
|
||||
key_list.append(key)
|
||||
for key in key_list:
|
||||
if key in ['enable'] or key not in user:
|
||||
continue
|
||||
ret += '\n'
|
||||
if (muid is not None) and (key in ['protocol_param']):
|
||||
for row in self.data.json:
|
||||
if int(row['port']) == muid:
|
||||
ret += " %s : %s" % (key, str(muid) + ':' + row['passwd'])
|
||||
break
|
||||
elif key in ['transfer_enable', 'u', 'd']:
|
||||
if muid is not None:
|
||||
for row in self.data.json:
|
||||
if int(row['port']) == muid:
|
||||
val = row[key]
|
||||
break
|
||||
else:
|
||||
val = user[key]
|
||||
if val / 1024 < 4:
|
||||
ret += " %s : %s" % (key, val)
|
||||
elif val / 1024 ** 2 < 4:
|
||||
val /= float(1024)
|
||||
ret += " %s : %s K Bytes" % (key, val)
|
||||
elif val / 1024 ** 3 < 4:
|
||||
val /= float(1024 ** 2)
|
||||
ret += " %s : %s M Bytes" % (key, val)
|
||||
else:
|
||||
val /= float(1024 ** 3)
|
||||
ret += " %s : %s G Bytes" % (key, val)
|
||||
else:
|
||||
ret += " %s : %s" % (key, user[key])
|
||||
ret += "\n " + self.ssrlink(user, False, muid)
|
||||
ret += "\n " + self.ssrlink(user, True, muid)
|
||||
return ret
|
||||
|
||||
def rand_pass(self):
|
||||
return ''.join([random.choice('''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~-_=+(){}[]^&%$@''') for i in range(8)])
|
||||
|
||||
def add(self, user):
|
||||
up = {'enable': 1, 'u': 0, 'd': 0, 'method': "aes-128-ctr",
|
||||
'protocol': "auth_aes128_md5",
|
||||
'obfs': "tls1.2_ticket_auth_compatible",
|
||||
'transfer_enable': 9007199254740992}
|
||||
up['passwd'] = self.rand_pass()
|
||||
up.update(user)
|
||||
|
||||
self.data.load(self.config_path)
|
||||
for row in self.data.json:
|
||||
match = False
|
||||
if 'user' in user and row['user'] == user['user']:
|
||||
match = True
|
||||
if 'port' in user and row['port'] == user['port']:
|
||||
match = True
|
||||
if match:
|
||||
print("user [%s] port [%s] already exist" % (row['user'], row['port']))
|
||||
return
|
||||
self.data.json.append(up)
|
||||
print("### add user info %s" % self.userinfo(up))
|
||||
self.data.save(self.config_path)
|
||||
|
||||
def edit(self, user):
|
||||
self.data.load(self.config_path)
|
||||
for row in self.data.json:
|
||||
match = True
|
||||
if 'user' in user and row['user'] != user['user']:
|
||||
match = False
|
||||
if 'port' in user and row['port'] != user['port']:
|
||||
match = False
|
||||
if match:
|
||||
print("edit user [%s]" % (row['user'],))
|
||||
row.update(user)
|
||||
print("### new user info %s" % self.userinfo(row))
|
||||
break
|
||||
self.data.save(self.config_path)
|
||||
|
||||
def delete(self, user):
|
||||
self.data.load(self.config_path)
|
||||
index = 0
|
||||
for row in self.data.json:
|
||||
match = True
|
||||
if 'user' in user and row['user'] != user['user']:
|
||||
match = False
|
||||
if 'port' in user and row['port'] != user['port']:
|
||||
match = False
|
||||
if match:
|
||||
print("delete user [%s]" % row['user'])
|
||||
del self.data.json[index]
|
||||
break
|
||||
index += 1
|
||||
self.data.save(self.config_path)
|
||||
|
||||
def clear_ud(self, user):
|
||||
up = {'u': 0, 'd': 0}
|
||||
self.data.load(self.config_path)
|
||||
for row in self.data.json:
|
||||
match = True
|
||||
if 'user' in user and row['user'] != user['user']:
|
||||
match = False
|
||||
if 'port' in user and row['port'] != user['port']:
|
||||
match = False
|
||||
if match:
|
||||
row.update(up)
|
||||
print("clear user [%s]" % row['user'])
|
||||
self.data.save(self.config_path)
|
||||
|
||||
def list_user(self, user):
|
||||
self.data.load(self.config_path)
|
||||
if not user:
|
||||
for row in self.data.json:
|
||||
print("user [%s] port %s" % (row['user'], row['port']))
|
||||
return
|
||||
for row in self.data.json:
|
||||
match = True
|
||||
if 'user' in user and row['user'] != user['user']:
|
||||
match = False
|
||||
if 'port' in user and row['port'] != user['port']:
|
||||
match = False
|
||||
if match:
|
||||
muid = None
|
||||
if 'muid' in user:
|
||||
muid = user['muid']
|
||||
print("### user [%s] info %s" % (row['user'], self.userinfo(row, muid)))
|
||||
|
||||
|
||||
def print_server_help():
|
||||
print('''usage: python3 mujson_manage.py -a|-d|-e|-c|-l [OPTION]...
|
||||
|
||||
Actions:
|
||||
-a add/edit a user
|
||||
-d delete a user
|
||||
-e edit a user
|
||||
-c set u&d to zero
|
||||
-l display a user infomation or all users infomation
|
||||
|
||||
Options:
|
||||
-u USER the user name
|
||||
-p PORT server port (only this option must be set if add a user)
|
||||
-k PASSWORD password
|
||||
-m METHOD encryption method, default: aes-128-ctr
|
||||
-O PROTOCOL protocol plugin, default: auth_aes128_md5
|
||||
-o OBFS obfs plugin, default: tls1.2_ticket_auth_compatible
|
||||
-G PROTOCOL_PARAM protocol plugin param
|
||||
-g OBFS_PARAM obfs plugin param
|
||||
-t TRANSFER max transfer for G bytes, default: 8388608 (8 PB or 8192 TB)
|
||||
-f FORBID set forbidden ports. Example (ban 1~79 and 81~100): -f "1-79,81-100"
|
||||
-i MUID set sub id to display (only work with -l)
|
||||
-s SPEED set speed_limit_per_con
|
||||
-S SPEED set speed_limit_per_user
|
||||
|
||||
General options:
|
||||
-h, --help show this help message and exit
|
||||
''')
|
||||
|
||||
|
||||
def main():
|
||||
shortopts = 'adeclu:i:p:k:O:o:G:g:m:t:f:hs:S:'
|
||||
longopts = ['help']
|
||||
action = None
|
||||
user = {}
|
||||
fast_set_obfs = {'0': 'plain',
|
||||
'+1': 'http_simple_compatible',
|
||||
'1': 'http_simple',
|
||||
'+2': 'tls1.2_ticket_auth_compatible',
|
||||
'2': 'tls1.2_ticket_auth'}
|
||||
fast_set_protocol = {'0': 'origin',
|
||||
's4': 'auth_sha1_v4',
|
||||
'+s4': 'auth_sha1_v4_compatible',
|
||||
'am': 'auth_aes128_md5',
|
||||
'as': 'auth_aes128_sha1',
|
||||
'ca': 'auth_chain_a',
|
||||
}
|
||||
fast_set_method = {'0': 'none',
|
||||
'a1c': 'aes-128-cfb',
|
||||
'a2c': 'aes-192-cfb',
|
||||
'a3c': 'aes-256-cfb',
|
||||
'r': 'rc4-md5',
|
||||
'r6': 'rc4-md5-6',
|
||||
'c': 'chacha20',
|
||||
'ci': 'chacha20-ietf',
|
||||
's': 'salsa20',
|
||||
'a1': 'aes-128-ctr',
|
||||
'a2': 'aes-192-ctr',
|
||||
'a3': 'aes-256-ctr'}
|
||||
try:
|
||||
optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
|
||||
for key, value in optlist:
|
||||
if key == '-a':
|
||||
action = 1
|
||||
elif key == '-d':
|
||||
action = 2
|
||||
elif key == '-e':
|
||||
action = 3
|
||||
elif key == '-l':
|
||||
action = 4
|
||||
elif key == '-c':
|
||||
action = 0
|
||||
elif key == '-u':
|
||||
user['user'] = value
|
||||
elif key == '-i':
|
||||
user['muid'] = int(value)
|
||||
elif key == '-p':
|
||||
user['port'] = int(value)
|
||||
elif key == '-k':
|
||||
user['passwd'] = value
|
||||
elif key == '-o':
|
||||
if value in fast_set_obfs:
|
||||
user['obfs'] = fast_set_obfs[value]
|
||||
else:
|
||||
user['obfs'] = value
|
||||
elif key == '-O':
|
||||
if value in fast_set_protocol:
|
||||
user['protocol'] = fast_set_protocol[value]
|
||||
else:
|
||||
user['protocol'] = value
|
||||
elif key == '-g':
|
||||
user['obfs_param'] = value
|
||||
elif key == '-G':
|
||||
user['protocol_param'] = value
|
||||
elif key == '-s':
|
||||
user['speed_limit_per_con'] = int(value)
|
||||
elif key == '-S':
|
||||
user['speed_limit_per_user'] = int(value)
|
||||
elif key == '-m':
|
||||
if value in fast_set_method:
|
||||
user['method'] = fast_set_method[value]
|
||||
else:
|
||||
user['method'] = value
|
||||
elif key == '-f':
|
||||
user['forbidden_port'] = value
|
||||
elif key == '-t':
|
||||
val = float(value)
|
||||
try:
|
||||
val = int(value)
|
||||
except:
|
||||
pass
|
||||
user['transfer_enable'] = int(val * 1024) * (1024 ** 2)
|
||||
elif key in ('-h', '--help'):
|
||||
print_server_help()
|
||||
sys.exit(0)
|
||||
except getopt.GetoptError as e:
|
||||
print(e)
|
||||
sys.exit(2)
|
||||
|
||||
manage = MuMgr()
|
||||
if action == 0:
|
||||
manage.clear_ud(user)
|
||||
elif action == 1:
|
||||
if 'user' not in user and 'port' in user:
|
||||
user['user'] = str(user['port'])
|
||||
if 'user' in user and 'port' in user:
|
||||
manage.add(user)
|
||||
else:
|
||||
print("You have to set the port with -p")
|
||||
elif action == 2:
|
||||
if 'user' in user or 'port' in user:
|
||||
manage.delete(user)
|
||||
else:
|
||||
print("You have to set the user name or port with -u/-p")
|
||||
elif action == 3:
|
||||
if 'user' in user or 'port' in user:
|
||||
manage.edit(user)
|
||||
else:
|
||||
print("You have to set the user name or port with -u/-p")
|
||||
elif action == 4:
|
||||
manage.list_user(user)
|
||||
elif action is None:
|
||||
print_server_help()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"host": "127.0.0.1",
|
||||
"port": 3306,
|
||||
"user": "ss",
|
||||
"password": "pass",
|
||||
"db": "sspanel",
|
||||
"node_id": 0,
|
||||
"transfer_mul": 1.0,
|
||||
"ssl_enable": 0,
|
||||
"ssl_ca": "",
|
||||
"ssl_cert": "",
|
||||
"ssl_key": ""
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
cd `dirname $0`
|
||||
#python3_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1)
|
||||
eval $(ps -ef | grep "[0-9] python3 server\\.py m" | awk '{print "kill "$2}')
|
||||
ulimit -n 512000
|
||||
nohup python3 server.py m>> /dev/null 2>&1 &
|
||||
|
@ -0,0 +1,66 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2015 breakwall
|
||||
#
|
||||
# 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.
|
||||
|
||||
import time
|
||||
import sys
|
||||
import threading
|
||||
import os
|
||||
|
||||
if __name__ == '__main__':
|
||||
import inspect
|
||||
os.chdir(os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe()))))
|
||||
|
||||
import server_pool
|
||||
import db_transfer
|
||||
from shadowsocks import shell
|
||||
from configloader import load_config, get_config
|
||||
|
||||
class MainThread(threading.Thread):
|
||||
def __init__(self, obj):
|
||||
super(MainThread, self).__init__()
|
||||
self.daemon = True
|
||||
self.obj = obj
|
||||
|
||||
def run(self):
|
||||
self.obj.thread_db(self.obj)
|
||||
|
||||
def stop(self):
|
||||
self.obj.thread_db_stop()
|
||||
|
||||
def main():
|
||||
shell.check_python3()
|
||||
if False:
|
||||
db_transfer.DbTransfer.thread_db()
|
||||
else:
|
||||
if get_config().API_INTERFACE == 'mudbjson':
|
||||
thread = MainThread(db_transfer.MuJsonTransfer)
|
||||
elif get_config().API_INTERFACE == 'sspanelv2':
|
||||
thread = MainThread(db_transfer.DbTransfer)
|
||||
else:
|
||||
thread = MainThread(db_transfer.Dbv3Transfer)
|
||||
thread.start()
|
||||
try:
|
||||
while thread.is_alive():
|
||||
thread.join(10.0)
|
||||
except (KeyboardInterrupt, IOError, OSError) as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
thread.stop()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import logging
|
||||
import struct
|
||||
import time
|
||||
from shadowsocks import shell, eventloop, tcprelay, udprelay, asyncdns, common
|
||||
import threading
|
||||
import sys
|
||||
import traceback
|
||||
from socket import *
|
||||
from configloader import load_config, get_config
|
||||
|
||||
class MainThread(threading.Thread):
|
||||
def __init__(self, params):
|
||||
super(MainThread, self).__init__()
|
||||
self.params = params
|
||||
|
||||
def run(self):
|
||||
ServerPool._loop(*self.params)
|
||||
|
||||
class ServerPool(object):
|
||||
|
||||
instance = None
|
||||
|
||||
def __init__(self):
|
||||
shell.check_python3()
|
||||
self.config = shell.get_config(False)
|
||||
self.dns_resolver = asyncdns.DNSResolver()
|
||||
if not self.config.get('dns_ipv6', False):
|
||||
asyncdns.IPV6_CONNECTION_SUPPORT = False
|
||||
|
||||
self.mgr = None #asyncmgr.ServerMgr()
|
||||
|
||||
self.tcp_servers_pool = {}
|
||||
self.tcp_ipv6_servers_pool = {}
|
||||
self.udp_servers_pool = {}
|
||||
self.udp_ipv6_servers_pool = {}
|
||||
self.stat_counter = {}
|
||||
|
||||
self.loop = eventloop.EventLoop()
|
||||
self.thread = MainThread( (self.loop, self.dns_resolver, self.mgr) )
|
||||
self.thread.start()
|
||||
|
||||
@staticmethod
|
||||
def get_instance():
|
||||
if ServerPool.instance is None:
|
||||
ServerPool.instance = ServerPool()
|
||||
return ServerPool.instance
|
||||
|
||||
def stop(self):
|
||||
self.loop.stop()
|
||||
|
||||
@staticmethod
|
||||
def _loop(loop, dns_resolver, mgr):
|
||||
try:
|
||||
if mgr is not None:
|
||||
mgr.add_to_loop(loop)
|
||||
dns_resolver.add_to_loop(loop)
|
||||
loop.run()
|
||||
except (KeyboardInterrupt, IOError, OSError) as e:
|
||||
logging.error(e)
|
||||
traceback.print_exc()
|
||||
os.exit(0)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
traceback.print_exc()
|
||||
|
||||
def server_is_run(self, port):
|
||||
port = int(port)
|
||||
ret = 0
|
||||
if port in self.tcp_servers_pool:
|
||||
ret = 1
|
||||
if port in self.tcp_ipv6_servers_pool:
|
||||
ret |= 2
|
||||
return ret
|
||||
|
||||
def server_run_status(self, port):
|
||||
if 'server' in self.config:
|
||||
if port not in self.tcp_servers_pool:
|
||||
return False
|
||||
if 'server_ipv6' in self.config:
|
||||
if port not in self.tcp_ipv6_servers_pool:
|
||||
return False
|
||||
return True
|
||||
|
||||
def new_server(self, port, user_config):
|
||||
ret = True
|
||||
port = int(port)
|
||||
ipv6_ok = False
|
||||
|
||||
if 'server_ipv6' in self.config:
|
||||
if port in self.tcp_ipv6_servers_pool:
|
||||
logging.info("server already at %s:%d" % (self.config['server_ipv6'], port))
|
||||
return 'this port server is already running'
|
||||
else:
|
||||
a_config = self.config.copy()
|
||||
a_config.update(user_config)
|
||||
if len(a_config['server_ipv6']) > 2 and a_config['server_ipv6'][0] == "[" and a_config['server_ipv6'][-1] == "]":
|
||||
a_config['server_ipv6'] = a_config['server_ipv6'][1:-1]
|
||||
a_config['server'] = a_config['server_ipv6']
|
||||
a_config['server_port'] = port
|
||||
a_config['max_connect'] = 128
|
||||
a_config['method'] = common.to_str(a_config['method'])
|
||||
try:
|
||||
logging.info("starting server at [%s]:%d" % (common.to_str(a_config['server']), port))
|
||||
|
||||
tcp_server = tcprelay.TCPRelay(a_config, self.dns_resolver, False, stat_counter=self.stat_counter)
|
||||
tcp_server.add_to_loop(self.loop)
|
||||
self.tcp_ipv6_servers_pool.update({port: tcp_server})
|
||||
|
||||
udp_server = udprelay.UDPRelay(a_config, self.dns_resolver, False, stat_counter=self.stat_counter)
|
||||
udp_server.add_to_loop(self.loop)
|
||||
self.udp_ipv6_servers_pool.update({port: udp_server})
|
||||
|
||||
if common.to_str(a_config['server_ipv6']) == "::":
|
||||
ipv6_ok = True
|
||||
except Exception as e:
|
||||
logging.warn("IPV6 %s " % (e,))
|
||||
|
||||
if 'server' in self.config:
|
||||
if port in self.tcp_servers_pool:
|
||||
logging.info("server already at %s:%d" % (common.to_str(self.config['server']), port))
|
||||
return 'this port server is already running'
|
||||
else:
|
||||
a_config = self.config.copy()
|
||||
a_config.update(user_config)
|
||||
a_config['server_port'] = port
|
||||
a_config['max_connect'] = 128
|
||||
a_config['method'] = common.to_str(a_config['method'])
|
||||
try:
|
||||
logging.info("starting server at %s:%d" % (common.to_str(a_config['server']), port))
|
||||
|
||||
tcp_server = tcprelay.TCPRelay(a_config, self.dns_resolver, False)
|
||||
tcp_server.add_to_loop(self.loop)
|
||||
self.tcp_servers_pool.update({port: tcp_server})
|
||||
|
||||
udp_server = udprelay.UDPRelay(a_config, self.dns_resolver, False)
|
||||
udp_server.add_to_loop(self.loop)
|
||||
self.udp_servers_pool.update({port: udp_server})
|
||||
|
||||
except Exception as e:
|
||||
if not ipv6_ok:
|
||||
logging.warn("IPV4 %s " % (e,))
|
||||
|
||||
return True
|
||||
|
||||
def del_server(self, port):
|
||||
port = int(port)
|
||||
logging.info("del server at %d" % port)
|
||||
try:
|
||||
udpsock = socket(AF_INET, SOCK_DGRAM)
|
||||
udpsock.sendto('%s:%s:0:0' % (get_config().MANAGE_PASS, port), (get_config().MANAGE_BIND_IP, get_config().MANAGE_PORT))
|
||||
udpsock.close()
|
||||
except Exception as e:
|
||||
logging.warn(e)
|
||||
return True
|
||||
|
||||
def cb_del_server(self, port):
|
||||
port = int(port)
|
||||
|
||||
if port not in self.tcp_servers_pool:
|
||||
logging.info("stopped server at %s:%d already stop" % (self.config['server'], port))
|
||||
else:
|
||||
logging.info("stopped server at %s:%d" % (self.config['server'], port))
|
||||
try:
|
||||
self.tcp_servers_pool[port].close(True)
|
||||
del self.tcp_servers_pool[port]
|
||||
except Exception as e:
|
||||
logging.warn(e)
|
||||
try:
|
||||
self.udp_servers_pool[port].close(True)
|
||||
del self.udp_servers_pool[port]
|
||||
except Exception as e:
|
||||
logging.warn(e)
|
||||
|
||||
if 'server_ipv6' in self.config:
|
||||
if port not in self.tcp_ipv6_servers_pool:
|
||||
logging.info("stopped server at [%s]:%d already stop" % (self.config['server_ipv6'], port))
|
||||
else:
|
||||
logging.info("stopped server at [%s]:%d" % (self.config['server_ipv6'], port))
|
||||
try:
|
||||
self.tcp_ipv6_servers_pool[port].close(True)
|
||||
del self.tcp_ipv6_servers_pool[port]
|
||||
except Exception as e:
|
||||
logging.warn(e)
|
||||
try:
|
||||
self.udp_ipv6_servers_pool[port].close(True)
|
||||
del self.udp_ipv6_servers_pool[port]
|
||||
except Exception as e:
|
||||
logging.warn(e)
|
||||
|
||||
return True
|
||||
|
||||
def update_mu_users(self, port, users):
|
||||
port = int(port)
|
||||
if port in self.tcp_servers_pool:
|
||||
try:
|
||||
self.tcp_servers_pool[port].update_users(users)
|
||||
except Exception as e:
|
||||
logging.warn(e)
|
||||
try:
|
||||
self.udp_servers_pool[port].update_users(users)
|
||||
except Exception as e:
|
||||
logging.warn(e)
|
||||
if port in self.tcp_ipv6_servers_pool:
|
||||
try:
|
||||
self.tcp_ipv6_servers_pool[port].update_users(users)
|
||||
except Exception as e:
|
||||
logging.warn(e)
|
||||
try:
|
||||
self.udp_ipv6_servers_pool[port].update_users(users)
|
||||
except Exception as e:
|
||||
logging.warn(e)
|
||||
|
||||
def get_server_transfer(self, port):
|
||||
port = int(port)
|
||||
uid = struct.pack('<I', port)
|
||||
ret = [0, 0]
|
||||
if port in self.tcp_servers_pool:
|
||||
ret[0], ret[1] = self.tcp_servers_pool[port].get_ud()
|
||||
if port in self.udp_servers_pool:
|
||||
u, d = self.udp_servers_pool[port].get_ud()
|
||||
ret[0] += u
|
||||
ret[1] += d
|
||||
if port in self.tcp_ipv6_servers_pool:
|
||||
u, d = self.tcp_ipv6_servers_pool[port].get_ud()
|
||||
ret[0] += u
|
||||
ret[1] += d
|
||||
if port in self.udp_ipv6_servers_pool:
|
||||
u, d = self.udp_ipv6_servers_pool[port].get_ud()
|
||||
ret[0] += u
|
||||
ret[1] += d
|
||||
return ret
|
||||
|
||||
def get_server_mu_transfer(self, server):
|
||||
return server.get_users_ud()
|
||||
|
||||
def update_mu_transfer(self, user_dict, u, d):
|
||||
for uid in u:
|
||||
port = struct.unpack('<I', uid)[0]
|
||||
if port not in user_dict:
|
||||
user_dict[port] = [0, 0]
|
||||
user_dict[port][0] += u[uid]
|
||||
for uid in d:
|
||||
port = struct.unpack('<I', uid)[0]
|
||||
if port not in user_dict:
|
||||
user_dict[port] = [0, 0]
|
||||
user_dict[port][1] += d[uid]
|
||||
|
||||
def get_servers_transfer(self):
|
||||
servers = self.tcp_servers_pool.copy()
|
||||
servers.update(self.tcp_ipv6_servers_pool)
|
||||
servers.update(self.udp_servers_pool)
|
||||
servers.update(self.udp_ipv6_servers_pool)
|
||||
ret = {}
|
||||
for port in servers.keys():
|
||||
ret[port] = self.get_server_transfer(port)
|
||||
for port in self.tcp_servers_pool:
|
||||
u, d = self.get_server_mu_transfer(self.tcp_servers_pool[port])
|
||||
self.update_mu_transfer(ret, u, d)
|
||||
for port in self.tcp_ipv6_servers_pool:
|
||||
u, d = self.get_server_mu_transfer(self.tcp_ipv6_servers_pool[port])
|
||||
self.update_mu_transfer(ret, u, d)
|
||||
for port in self.udp_servers_pool:
|
||||
u, d = self.get_server_mu_transfer(self.udp_servers_pool[port])
|
||||
self.update_mu_transfer(ret, u, d)
|
||||
for port in self.udp_ipv6_servers_pool:
|
||||
u, d = self.get_server_mu_transfer(self.udp_ipv6_servers_pool[port])
|
||||
self.update_mu_transfer(ret, u, d)
|
||||
return ret
|
||||
|
@ -0,0 +1,39 @@
|
||||
import codecs
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
with codecs.open('README.rst', encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name="shadowsocks",
|
||||
version="2.6.12",
|
||||
license='http://www.apache.org/licenses/LICENSE-2.0',
|
||||
description="A fast tunnel proxy that help you get through firewalls",
|
||||
author='clowwindy',
|
||||
author_email='clowwindy42@gmail.com',
|
||||
url='https://github.com/shadowsocks/shadowsocks',
|
||||
packages=['shadowsocks', 'shadowsocks.crypto', 'shadowsocks.obfsplugin'],
|
||||
package_data={
|
||||
'shadowsocks': ['README.rst', 'LICENSE']
|
||||
},
|
||||
install_requires=[],
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
sslocal = shadowsocks.local:main
|
||||
ssserver = shadowsocks.server:main
|
||||
""",
|
||||
classifiers=[
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: Implementation :: CPython',
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
'Topic :: Internet :: Proxy Servers',
|
||||
],
|
||||
long_description=long_description,
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
rm -rf CyMySQL
|
||||
rm -rf cymysql
|
||||
git clone https://github.com/nakagami/CyMySQL.git
|
||||
mv CyMySQL/cymysql ./
|
||||
rm -rf CyMySQL
|
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/python3
|
||||
#
|
||||
# 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
|
@ -0,0 +1,555 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014-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 os
|
||||
import socket
|
||||
import struct
|
||||
import re
|
||||
import logging
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import inspect
|
||||
file_path = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe())))
|
||||
sys.path.insert(0, os.path.join(file_path, '../'))
|
||||
|
||||
from shadowsocks import common, lru_cache, eventloop, shell
|
||||
|
||||
|
||||
CACHE_SWEEP_INTERVAL = 30
|
||||
|
||||
VALID_HOSTNAME = re.compile(br"(?!-)[A-Z\d_-]{1,63}(?<!-)$", re.IGNORECASE)
|
||||
|
||||
common.patch_socket()
|
||||
|
||||
# rfc1035
|
||||
# format
|
||||
# +---------------------+
|
||||
# | Header |
|
||||
# +---------------------+
|
||||
# | Question | the question for the name server
|
||||
# +---------------------+
|
||||
# | Answer | RRs answering the question
|
||||
# +---------------------+
|
||||
# | Authority | RRs pointing toward an authority
|
||||
# +---------------------+
|
||||
# | Additional | RRs holding additional information
|
||||
# +---------------------+
|
||||
#
|
||||
# header
|
||||
# 1 1 1 1 1 1
|
||||
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# | ID |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# | QDCOUNT |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# | ANCOUNT |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# | NSCOUNT |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# | ARCOUNT |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
|
||||
QTYPE_ANY = 255
|
||||
QTYPE_A = 1
|
||||
QTYPE_AAAA = 28
|
||||
QTYPE_CNAME = 5
|
||||
QTYPE_NS = 2
|
||||
QCLASS_IN = 1
|
||||
|
||||
def detect_ipv6_supprot():
|
||||
if 'has_ipv6' in dir(socket):
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
s.connect(('::1', 0))
|
||||
print('IPv6 support')
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
print('IPv6 not support')
|
||||
return False
|
||||
|
||||
IPV6_CONNECTION_SUPPORT = detect_ipv6_supprot()
|
||||
|
||||
def build_address(address):
|
||||
address = address.strip(b'.')
|
||||
labels = address.split(b'.')
|
||||
results = []
|
||||
for label in labels:
|
||||
l = len(label)
|
||||
if l > 63:
|
||||
return None
|
||||
results.append(common.chr(l))
|
||||
results.append(label)
|
||||
results.append(b'\0')
|
||||
return b''.join(results)
|
||||
|
||||
|
||||
def build_request(address, qtype):
|
||||
request_id = os.urandom(2)
|
||||
header = struct.pack('!BBHHHH', 1, 0, 1, 0, 0, 0)
|
||||
addr = build_address(address)
|
||||
qtype_qclass = struct.pack('!HH', qtype, QCLASS_IN)
|
||||
return request_id + header + addr + qtype_qclass
|
||||
|
||||
|
||||
def parse_ip(addrtype, data, length, offset):
|
||||
if addrtype == QTYPE_A:
|
||||
return socket.inet_ntop(socket.AF_INET, data[offset:offset + length])
|
||||
elif addrtype == QTYPE_AAAA:
|
||||
return socket.inet_ntop(socket.AF_INET6, data[offset:offset + length])
|
||||
elif addrtype in [QTYPE_CNAME, QTYPE_NS]:
|
||||
return parse_name(data, offset)[1]
|
||||
else:
|
||||
return data[offset:offset + length]
|
||||
|
||||
|
||||
def parse_name(data, offset):
|
||||
p = offset
|
||||
labels = []
|
||||
l = common.ord(data[p])
|
||||
while l > 0:
|
||||
if (l & (128 + 64)) == (128 + 64):
|
||||
# pointer
|
||||
pointer = struct.unpack('!H', data[p:p + 2])[0]
|
||||
pointer &= 0x3FFF
|
||||
r = parse_name(data, pointer)
|
||||
labels.append(r[1])
|
||||
p += 2
|
||||
# pointer is the end
|
||||
return p - offset, b'.'.join(labels)
|
||||
else:
|
||||
labels.append(data[p + 1:p + 1 + l])
|
||||
p += 1 + l
|
||||
l = common.ord(data[p])
|
||||
return p - offset + 1, b'.'.join(labels)
|
||||
|
||||
|
||||
# rfc1035
|
||||
# record
|
||||
# 1 1 1 1 1 1
|
||||
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# | |
|
||||
# / /
|
||||
# / NAME /
|
||||
# | |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# | TYPE |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# | CLASS |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# | TTL |
|
||||
# | |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
# | RDLENGTH |
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
|
||||
# / RDATA /
|
||||
# / /
|
||||
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
def parse_record(data, offset, question=False):
|
||||
nlen, name = parse_name(data, offset)
|
||||
if not question:
|
||||
record_type, record_class, record_ttl, record_rdlength = struct.unpack(
|
||||
'!HHiH', data[offset + nlen:offset + nlen + 10]
|
||||
)
|
||||
ip = parse_ip(record_type, data, record_rdlength, offset + nlen + 10)
|
||||
return nlen + 10 + record_rdlength, \
|
||||
(name, ip, record_type, record_class, record_ttl)
|
||||
else:
|
||||
record_type, record_class = struct.unpack(
|
||||
'!HH', data[offset + nlen:offset + nlen + 4]
|
||||
)
|
||||
return nlen + 4, (name, None, record_type, record_class, None, None)
|
||||
|
||||
|
||||
def parse_header(data):
|
||||
if len(data) >= 12:
|
||||
header = struct.unpack('!HBBHHHH', data[:12])
|
||||
res_id = header[0]
|
||||
res_qr = header[1] & 128
|
||||
res_tc = header[1] & 2
|
||||
res_ra = header[2] & 128
|
||||
res_rcode = header[2] & 15
|
||||
# assert res_tc == 0
|
||||
# assert res_rcode in [0, 3]
|
||||
res_qdcount = header[3]
|
||||
res_ancount = header[4]
|
||||
res_nscount = header[5]
|
||||
res_arcount = header[6]
|
||||
return (res_id, res_qr, res_tc, res_ra, res_rcode, res_qdcount,
|
||||
res_ancount, res_nscount, res_arcount)
|
||||
return None
|
||||
|
||||
|
||||
def parse_response(data):
|
||||
try:
|
||||
if len(data) >= 12:
|
||||
header = parse_header(data)
|
||||
if not header:
|
||||
return None
|
||||
res_id, res_qr, res_tc, res_ra, res_rcode, res_qdcount, \
|
||||
res_ancount, res_nscount, res_arcount = header
|
||||
|
||||
qds = []
|
||||
ans = []
|
||||
offset = 12
|
||||
for i in range(0, res_qdcount):
|
||||
l, r = parse_record(data, offset, True)
|
||||
offset += l
|
||||
if r:
|
||||
qds.append(r)
|
||||
for i in range(0, res_ancount):
|
||||
l, r = parse_record(data, offset)
|
||||
offset += l
|
||||
if r:
|
||||
ans.append(r)
|
||||
for i in range(0, res_nscount):
|
||||
l, r = parse_record(data, offset)
|
||||
offset += l
|
||||
for i in range(0, res_arcount):
|
||||
l, r = parse_record(data, offset)
|
||||
offset += l
|
||||
response = DNSResponse()
|
||||
if qds:
|
||||
response.hostname = qds[0][0]
|
||||
for an in qds:
|
||||
response.questions.append((an[1], an[2], an[3]))
|
||||
for an in ans:
|
||||
response.answers.append((an[1], an[2], an[3]))
|
||||
return response
|
||||
except Exception as e:
|
||||
shell.print_exception(e)
|
||||
return None
|
||||
|
||||
|
||||
def is_valid_hostname(hostname):
|
||||
if len(hostname) > 255:
|
||||
return False
|
||||
if hostname[-1] == b'.':
|
||||
hostname = hostname[:-1]
|
||||
return all(VALID_HOSTNAME.match(x) for x in hostname.split(b'.'))
|
||||
|
||||
|
||||
class DNSResponse(object):
|
||||
def __init__(self):
|
||||
self.hostname = None
|
||||
self.questions = [] # each: (addr, type, class)
|
||||
self.answers = [] # each: (addr, type, class)
|
||||
|
||||
def __str__(self):
|
||||
return '%s: %s' % (self.hostname, str(self.answers))
|
||||
|
||||
|
||||
STATUS_IPV4 = 0
|
||||
STATUS_IPV6 = 1
|
||||
|
||||
|
||||
class DNSResolver(object):
|
||||
|
||||
def __init__(self):
|
||||
self._loop = None
|
||||
self._hosts = {}
|
||||
self._hostname_status = {}
|
||||
self._hostname_to_cb = {}
|
||||
self._cb_to_hostname = {}
|
||||
self._cache = lru_cache.LRUCache(timeout=300)
|
||||
self._sock = None
|
||||
self._servers = None
|
||||
self._parse_resolv()
|
||||
self._parse_hosts()
|
||||
# TODO monitor hosts change and reload hosts
|
||||
# TODO parse /etc/gai.conf and follow its rules
|
||||
|
||||
def _parse_resolv(self):
|
||||
self._servers = []
|
||||
try:
|
||||
with open('dns.conf', 'rb') as f:
|
||||
content = f.readlines()
|
||||
for line in content:
|
||||
line = line.strip()
|
||||
if line:
|
||||
parts = line.split(b' ', 1)
|
||||
if len(parts) >= 2:
|
||||
server = parts[0]
|
||||
port = int(parts[1])
|
||||
else:
|
||||
server = parts[0]
|
||||
port = 53
|
||||
if common.is_ip(server) == socket.AF_INET:
|
||||
if type(server) != str:
|
||||
server = server.decode('utf8')
|
||||
self._servers.append((server, port))
|
||||
except IOError:
|
||||
pass
|
||||
if not self._servers:
|
||||
try:
|
||||
with open('/etc/resolv.conf', 'rb') as f:
|
||||
content = f.readlines()
|
||||
for line in content:
|
||||
line = line.strip()
|
||||
if line:
|
||||
if line.startswith(b'nameserver'):
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
server = parts[1]
|
||||
if common.is_ip(server) == socket.AF_INET:
|
||||
if type(server) != str:
|
||||
server = server.decode('utf8')
|
||||
self._servers.append((server, 53))
|
||||
except IOError:
|
||||
pass
|
||||
if not self._servers:
|
||||
self._servers = [('8.8.4.4', 53), ('8.8.8.8', 53)]
|
||||
logging.info('dns server: %s' % (self._servers,))
|
||||
|
||||
def _parse_hosts(self):
|
||||
etc_path = '/etc/hosts'
|
||||
if 'WINDIR' in os.environ:
|
||||
etc_path = os.environ['WINDIR'] + '/system32/drivers/etc/hosts'
|
||||
try:
|
||||
with open(etc_path, 'rb') as f:
|
||||
for line in f.readlines():
|
||||
line = line.strip()
|
||||
if b"#" in line:
|
||||
line = line[:line.find(b'#')]
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
ip = parts[0]
|
||||
if common.is_ip(ip):
|
||||
for i in range(1, len(parts)):
|
||||
hostname = parts[i]
|
||||
if hostname:
|
||||
self._hosts[hostname] = ip
|
||||
except IOError:
|
||||
self._hosts['localhost'] = '127.0.0.1'
|
||||
|
||||
def add_to_loop(self, loop):
|
||||
if self._loop:
|
||||
raise Exception('already add to loop')
|
||||
self._loop = loop
|
||||
# TODO when dns server is IPv6
|
||||
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
|
||||
socket.SOL_UDP)
|
||||
self._sock.setblocking(False)
|
||||
loop.add(self._sock, eventloop.POLL_IN, self)
|
||||
loop.add_periodic(self.handle_periodic)
|
||||
|
||||
def _call_callback(self, hostname, ip, error=None):
|
||||
callbacks = self._hostname_to_cb.get(hostname, [])
|
||||
for callback in callbacks:
|
||||
if callback in self._cb_to_hostname:
|
||||
del self._cb_to_hostname[callback]
|
||||
if ip or error:
|
||||
callback((hostname, ip), error)
|
||||
else:
|
||||
callback((hostname, None),
|
||||
Exception('unable to parse hostname %s' % hostname))
|
||||
if hostname in self._hostname_to_cb:
|
||||
del self._hostname_to_cb[hostname]
|
||||
if hostname in self._hostname_status:
|
||||
del self._hostname_status[hostname]
|
||||
|
||||
def _handle_data(self, data):
|
||||
response = parse_response(data)
|
||||
if response and response.hostname:
|
||||
hostname = response.hostname
|
||||
ip = None
|
||||
for answer in response.answers:
|
||||
if answer[1] in (QTYPE_A, QTYPE_AAAA) and \
|
||||
answer[2] == QCLASS_IN:
|
||||
ip = answer[0]
|
||||
break
|
||||
if IPV6_CONNECTION_SUPPORT:
|
||||
if not ip and self._hostname_status.get(hostname, STATUS_IPV4) \
|
||||
== STATUS_IPV6:
|
||||
self._hostname_status[hostname] = STATUS_IPV4
|
||||
self._send_req(hostname, QTYPE_A)
|
||||
else:
|
||||
if ip:
|
||||
self._cache[hostname] = ip
|
||||
self._call_callback(hostname, ip)
|
||||
elif self._hostname_status.get(hostname, None) == STATUS_IPV4:
|
||||
for question in response.questions:
|
||||
if question[1] == QTYPE_A:
|
||||
self._call_callback(hostname, None)
|
||||
break
|
||||
else:
|
||||
if not ip and self._hostname_status.get(hostname, STATUS_IPV6) \
|
||||
== STATUS_IPV4:
|
||||
self._hostname_status[hostname] = STATUS_IPV6
|
||||
self._send_req(hostname, QTYPE_AAAA)
|
||||
else:
|
||||
if ip:
|
||||
self._cache[hostname] = ip
|
||||
self._call_callback(hostname, ip)
|
||||
elif self._hostname_status.get(hostname, None) == STATUS_IPV6:
|
||||
for question in response.questions:
|
||||
if question[1] == QTYPE_AAAA:
|
||||
self._call_callback(hostname, None)
|
||||
break
|
||||
|
||||
def handle_event(self, sock, fd, event):
|
||||
if sock != self._sock:
|
||||
return
|
||||
if event & eventloop.POLL_ERR:
|
||||
logging.error('dns socket err')
|
||||
self._loop.remove(self._sock)
|
||||
self._sock.close()
|
||||
# TODO when dns server is IPv6
|
||||
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
|
||||
socket.SOL_UDP)
|
||||
self._sock.setblocking(False)
|
||||
self._loop.add(self._sock, eventloop.POLL_IN, self)
|
||||
else:
|
||||
data, addr = sock.recvfrom(1024)
|
||||
if addr not in self._servers:
|
||||
logging.warn('received a packet other than our dns')
|
||||
return
|
||||
self._handle_data(data)
|
||||
|
||||
def handle_periodic(self):
|
||||
self._cache.sweep()
|
||||
|
||||
def remove_callback(self, callback):
|
||||
hostname = self._cb_to_hostname.get(callback)
|
||||
if hostname:
|
||||
del self._cb_to_hostname[callback]
|
||||
arr = self._hostname_to_cb.get(hostname, None)
|
||||
if arr:
|
||||
arr.remove(callback)
|
||||
if not arr:
|
||||
del self._hostname_to_cb[hostname]
|
||||
if hostname in self._hostname_status:
|
||||
del self._hostname_status[hostname]
|
||||
|
||||
def _send_req(self, hostname, qtype):
|
||||
req = build_request(hostname, qtype)
|
||||
for server in self._servers:
|
||||
logging.debug('resolving %s with type %d using server %s',
|
||||
hostname, qtype, server)
|
||||
self._sock.sendto(req, server)
|
||||
|
||||
def resolve(self, hostname, callback):
|
||||
if type(hostname) != bytes:
|
||||
hostname = hostname.encode('utf8')
|
||||
if not hostname:
|
||||
callback(None, Exception('empty hostname'))
|
||||
elif common.is_ip(hostname):
|
||||
callback((hostname, hostname), None)
|
||||
elif hostname in self._hosts:
|
||||
logging.debug('hit hosts: %s', hostname)
|
||||
ip = self._hosts[hostname]
|
||||
callback((hostname, ip), None)
|
||||
elif hostname in self._cache:
|
||||
logging.debug('hit cache: %s', hostname)
|
||||
ip = self._cache[hostname]
|
||||
callback((hostname, ip), None)
|
||||
else:
|
||||
if not is_valid_hostname(hostname):
|
||||
callback(None, Exception('invalid hostname: %s' % hostname))
|
||||
return
|
||||
if False:
|
||||
addrs = socket.getaddrinfo(hostname, 0, 0,
|
||||
socket.SOCK_DGRAM, socket.SOL_UDP)
|
||||
if addrs:
|
||||
af, socktype, proto, canonname, sa = addrs[0]
|
||||
logging.debug('DNS resolve %s %s' % (hostname, sa[0]) )
|
||||
self._cache[hostname] = sa[0]
|
||||
callback((hostname, sa[0]), None)
|
||||
return
|
||||
arr = self._hostname_to_cb.get(hostname, None)
|
||||
if not arr:
|
||||
if IPV6_CONNECTION_SUPPORT:
|
||||
self._hostname_status[hostname] = STATUS_IPV6
|
||||
self._send_req(hostname, QTYPE_AAAA)
|
||||
else:
|
||||
self._hostname_status[hostname] = STATUS_IPV4
|
||||
self._send_req(hostname, QTYPE_A)
|
||||
self._hostname_to_cb[hostname] = [callback]
|
||||
self._cb_to_hostname[callback] = hostname
|
||||
else:
|
||||
arr.append(callback)
|
||||
# TODO send again only if waited too long
|
||||
if IPV6_CONNECTION_SUPPORT:
|
||||
self._send_req(hostname, QTYPE_AAAA)
|
||||
else:
|
||||
self._send_req(hostname, QTYPE_A)
|
||||
|
||||
def close(self):
|
||||
if self._sock:
|
||||
if self._loop:
|
||||
self._loop.remove_periodic(self.handle_periodic)
|
||||
self._loop.remove(self._sock)
|
||||
self._sock.close()
|
||||
self._sock = None
|
||||
|
||||
|
||||
def test():
|
||||
dns_resolver = DNSResolver()
|
||||
loop = eventloop.EventLoop()
|
||||
dns_resolver.add_to_loop(loop)
|
||||
|
||||
global counter
|
||||
counter = 0
|
||||
|
||||
def make_callback():
|
||||
global counter
|
||||
|
||||
def callback(result, error):
|
||||
global counter
|
||||
# TODO: what can we assert?
|
||||
print(result, error)
|
||||
counter += 1
|
||||
if counter == 9:
|
||||
dns_resolver.close()
|
||||
loop.stop()
|
||||
a_callback = callback
|
||||
return a_callback
|
||||
|
||||
assert(make_callback() != make_callback())
|
||||
|
||||
dns_resolver.resolve(b'google.com', make_callback())
|
||||
dns_resolver.resolve('google.com', make_callback())
|
||||
dns_resolver.resolve('example.com', make_callback())
|
||||
dns_resolver.resolve('ipv6.google.com', make_callback())
|
||||
dns_resolver.resolve('www.facebook.com', make_callback())
|
||||
dns_resolver.resolve('ns2.google.com', make_callback())
|
||||
dns_resolver.resolve('invalid.@!#$%^&$@.hostname', make_callback())
|
||||
dns_resolver.resolve('toooooooooooooooooooooooooooooooooooooooooooooooooo'
|
||||
'ooooooooooooooooooooooooooooooooooooooooooooooooooo'
|
||||
'long.hostname', make_callback())
|
||||
dns_resolver.resolve('toooooooooooooooooooooooooooooooooooooooooooooooooo'
|
||||
'ooooooooooooooooooooooooooooooooooooooooooooooooooo'
|
||||
'ooooooooooooooooooooooooooooooooooooooooooooooooooo'
|
||||
'ooooooooooooooooooooooooooooooooooooooooooooooooooo'
|
||||
'ooooooooooooooooooooooooooooooooooooooooooooooooooo'
|
||||
'ooooooooooooooooooooooooooooooooooooooooooooooooooo'
|
||||
'long.hostname', make_callback())
|
||||
|
||||
loop.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
|
@ -0,0 +1,418 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013-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 socket
|
||||
import struct
|
||||
import logging
|
||||
import binascii
|
||||
import re
|
||||
|
||||
from shadowsocks import lru_cache
|
||||
|
||||
def compat_ord(s):
|
||||
if type(s) == int:
|
||||
return s
|
||||
return _ord(s)
|
||||
|
||||
|
||||
def compat_chr(d):
|
||||
if bytes == str:
|
||||
return _chr(d)
|
||||
return bytes([d])
|
||||
|
||||
|
||||
_ord = ord
|
||||
_chr = chr
|
||||
ord = compat_ord
|
||||
chr = compat_chr
|
||||
|
||||
connect_log = logging.debug
|
||||
|
||||
def to_bytes(s):
|
||||
if bytes != str:
|
||||
if type(s) == str:
|
||||
return s.encode('utf-8')
|
||||
return s
|
||||
|
||||
|
||||
def to_str(s):
|
||||
if bytes != str:
|
||||
if type(s) == bytes:
|
||||
return s.decode('utf-8')
|
||||
return s
|
||||
|
||||
def int32(x):
|
||||
if x > 0xFFFFFFFF or x < 0:
|
||||
x &= 0xFFFFFFFF
|
||||
if x > 0x7FFFFFFF:
|
||||
x = int(0x100000000 - x)
|
||||
if x < 0x80000000:
|
||||
return -x
|
||||
else:
|
||||
return -2147483648
|
||||
return x
|
||||
|
||||
def inet_ntop(family, ipstr):
|
||||
if family == socket.AF_INET:
|
||||
return to_bytes(socket.inet_ntoa(ipstr))
|
||||
elif family == socket.AF_INET6:
|
||||
import re
|
||||
v6addr = ':'.join(('%02X%02X' % (ord(i), ord(j))).lstrip('0')
|
||||
for i, j in zip(ipstr[::2], ipstr[1::2]))
|
||||
v6addr = re.sub('::+', '::', v6addr, count=1)
|
||||
return to_bytes(v6addr)
|
||||
|
||||
|
||||
def inet_pton(family, addr):
|
||||
addr = to_str(addr)
|
||||
if family == socket.AF_INET:
|
||||
return socket.inet_aton(addr)
|
||||
elif family == socket.AF_INET6:
|
||||
if '.' in addr: # a v4 addr
|
||||
v4addr = addr[addr.rindex(':') + 1:]
|
||||
v4addr = socket.inet_aton(v4addr)
|
||||
v4addr = ['%02X' % ord(x) for x in v4addr]
|
||||
v4addr.insert(2, ':')
|
||||
newaddr = addr[:addr.rindex(':') + 1] + ''.join(v4addr)
|
||||
return inet_pton(family, newaddr)
|
||||
dbyts = [0] * 8 # 8 groups
|
||||
grps = addr.split(':')
|
||||
for i, v in enumerate(grps):
|
||||
if v:
|
||||
dbyts[i] = int(v, 16)
|
||||
else:
|
||||
for j, w in enumerate(grps[::-1]):
|
||||
if w:
|
||||
dbyts[7 - j] = int(w, 16)
|
||||
else:
|
||||
break
|
||||
break
|
||||
return b''.join((chr(i // 256) + chr(i % 256)) for i in dbyts)
|
||||
else:
|
||||
raise RuntimeError("What family?")
|
||||
|
||||
|
||||
def is_ip(address):
|
||||
for family in (socket.AF_INET, socket.AF_INET6):
|
||||
try:
|
||||
if type(address) != str:
|
||||
address = address.decode('utf8')
|
||||
inet_pton(family, address)
|
||||
return family
|
||||
except (TypeError, ValueError, OSError, IOError):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def match_regex(regex, text):
|
||||
regex = re.compile(regex)
|
||||
for item in regex.findall(text):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def patch_socket():
|
||||
if not hasattr(socket, 'inet_pton'):
|
||||
socket.inet_pton = inet_pton
|
||||
|
||||
if not hasattr(socket, 'inet_ntop'):
|
||||
socket.inet_ntop = inet_ntop
|
||||
|
||||
|
||||
patch_socket()
|
||||
|
||||
|
||||
ADDRTYPE_IPV4 = 1
|
||||
ADDRTYPE_IPV6 = 4
|
||||
ADDRTYPE_HOST = 3
|
||||
|
||||
|
||||
def pack_addr(address):
|
||||
address_str = to_str(address)
|
||||
for family in (socket.AF_INET, socket.AF_INET6):
|
||||
try:
|
||||
r = socket.inet_pton(family, address_str)
|
||||
if family == socket.AF_INET6:
|
||||
return b'\x04' + r
|
||||
else:
|
||||
return b'\x01' + r
|
||||
except (TypeError, ValueError, OSError, IOError):
|
||||
pass
|
||||
if len(address) > 255:
|
||||
address = address[:255] # TODO
|
||||
return b'\x03' + chr(len(address)) + address
|
||||
|
||||
def pre_parse_header(data):
|
||||
if not data:
|
||||
return None
|
||||
datatype = ord(data[0])
|
||||
if datatype == 0x80:
|
||||
if len(data) <= 2:
|
||||
return None
|
||||
rand_data_size = ord(data[1])
|
||||
if rand_data_size + 2 >= len(data):
|
||||
logging.warn('header too short, maybe wrong password or '
|
||||
'encryption method')
|
||||
return None
|
||||
data = data[rand_data_size + 2:]
|
||||
elif datatype == 0x81:
|
||||
data = data[1:]
|
||||
elif datatype == 0x82:
|
||||
if len(data) <= 3:
|
||||
return None
|
||||
rand_data_size = struct.unpack('>H', data[1:3])[0]
|
||||
if rand_data_size + 3 >= len(data):
|
||||
logging.warn('header too short, maybe wrong password or '
|
||||
'encryption method')
|
||||
return None
|
||||
data = data[rand_data_size + 3:]
|
||||
elif datatype == 0x88 or (~datatype & 0xff) == 0x88:
|
||||
if len(data) <= 7 + 7:
|
||||
return None
|
||||
data_size = struct.unpack('>H', data[1:3])[0]
|
||||
ogn_data = data
|
||||
data = data[:data_size]
|
||||
crc = binascii.crc32(data) & 0xffffffff
|
||||
if crc != 0xffffffff:
|
||||
logging.warn('uncorrect CRC32, maybe wrong password or '
|
||||
'encryption method')
|
||||
return None
|
||||
start_pos = 3 + ord(data[3])
|
||||
data = data[start_pos:-4]
|
||||
if data_size < len(ogn_data):
|
||||
data += ogn_data[data_size:]
|
||||
return data
|
||||
|
||||
def parse_header(data):
|
||||
addrtype = ord(data[0])
|
||||
dest_addr = None
|
||||
dest_port = None
|
||||
header_length = 0
|
||||
connecttype = (addrtype & 0x8) and 1 or 0
|
||||
addrtype &= ~0x8
|
||||
if addrtype == ADDRTYPE_IPV4:
|
||||
if len(data) >= 7:
|
||||
dest_addr = socket.inet_ntoa(data[1:5])
|
||||
dest_port = struct.unpack('>H', data[5:7])[0]
|
||||
header_length = 7
|
||||
else:
|
||||
logging.warn('header is too short')
|
||||
elif addrtype == ADDRTYPE_HOST:
|
||||
if len(data) > 2:
|
||||
addrlen = ord(data[1])
|
||||
if len(data) >= 4 + addrlen:
|
||||
dest_addr = data[2:2 + addrlen]
|
||||
dest_port = struct.unpack('>H', data[2 + addrlen:4 +
|
||||
addrlen])[0]
|
||||
header_length = 4 + addrlen
|
||||
else:
|
||||
logging.warn('header is too short')
|
||||
else:
|
||||
logging.warn('header is too short')
|
||||
elif addrtype == ADDRTYPE_IPV6:
|
||||
if len(data) >= 19:
|
||||
dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17])
|
||||
dest_port = struct.unpack('>H', data[17:19])[0]
|
||||
header_length = 19
|
||||
else:
|
||||
logging.warn('header is too short')
|
||||
else:
|
||||
logging.warn('unsupported addrtype %d, maybe wrong password or '
|
||||
'encryption method' % addrtype)
|
||||
if dest_addr is None:
|
||||
return None
|
||||
return connecttype, addrtype, to_bytes(dest_addr), dest_port, header_length
|
||||
|
||||
|
||||
class IPNetwork(object):
|
||||
ADDRLENGTH = {socket.AF_INET: 32, socket.AF_INET6: 128, False: 0}
|
||||
|
||||
def __init__(self, addrs):
|
||||
self.addrs_str = addrs
|
||||
self._network_list_v4 = []
|
||||
self._network_list_v6 = []
|
||||
if type(addrs) == str:
|
||||
addrs = addrs.split(',')
|
||||
list(map(self.add_network, addrs))
|
||||
|
||||
def add_network(self, addr):
|
||||
if addr is "":
|
||||
return
|
||||
block = addr.split('/')
|
||||
addr_family = is_ip(block[0])
|
||||
addr_len = IPNetwork.ADDRLENGTH[addr_family]
|
||||
if addr_family is socket.AF_INET:
|
||||
ip, = struct.unpack("!I", socket.inet_aton(block[0]))
|
||||
elif addr_family is socket.AF_INET6:
|
||||
hi, lo = struct.unpack("!QQ", inet_pton(addr_family, block[0]))
|
||||
ip = (hi << 64) | lo
|
||||
else:
|
||||
raise Exception("Not a valid CIDR notation: %s" % addr)
|
||||
if len(block) is 1:
|
||||
prefix_size = 0
|
||||
while (ip & 1) == 0 and ip is not 0:
|
||||
ip >>= 1
|
||||
prefix_size += 1
|
||||
logging.warn("You did't specify CIDR routing prefix size for %s, "
|
||||
"implicit treated as %s/%d" % (addr, addr, addr_len))
|
||||
elif block[1].isdigit() and int(block[1]) <= addr_len:
|
||||
prefix_size = addr_len - int(block[1])
|
||||
ip >>= prefix_size
|
||||
else:
|
||||
raise Exception("Not a valid CIDR notation: %s" % addr)
|
||||
if addr_family is socket.AF_INET:
|
||||
self._network_list_v4.append((ip, prefix_size))
|
||||
else:
|
||||
self._network_list_v6.append((ip, prefix_size))
|
||||
|
||||
def __contains__(self, addr):
|
||||
addr_family = is_ip(addr)
|
||||
if addr_family is socket.AF_INET:
|
||||
ip, = struct.unpack("!I", socket.inet_aton(addr))
|
||||
return any(map(lambda n_ps: n_ps[0] == ip >> n_ps[1],
|
||||
self._network_list_v4))
|
||||
elif addr_family is socket.AF_INET6:
|
||||
hi, lo = struct.unpack("!QQ", inet_pton(addr_family, addr))
|
||||
ip = (hi << 64) | lo
|
||||
return any(map(lambda n_ps: n_ps[0] == ip >> n_ps[1],
|
||||
self._network_list_v6))
|
||||
else:
|
||||
return False
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.addrs_str, other.addrs_str)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.addrs_str == other.addrs_str
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.addrs_str != other.addrs_str
|
||||
|
||||
class PortRange(object):
|
||||
def __init__(self, range_str):
|
||||
self.range_str = to_str(range_str)
|
||||
self.range = set()
|
||||
range_str = to_str(range_str).split(',')
|
||||
for item in range_str:
|
||||
try:
|
||||
int_range = item.split('-')
|
||||
if len(int_range) == 1:
|
||||
if item:
|
||||
self.range.add(int(item))
|
||||
elif len(int_range) == 2:
|
||||
int_range[0] = int(int_range[0])
|
||||
int_range[1] = int(int_range[1])
|
||||
if int_range[0] < 0:
|
||||
int_range[0] = 0
|
||||
if int_range[1] > 65535:
|
||||
int_range[1] = 65535
|
||||
i = int_range[0]
|
||||
while i <= int_range[1]:
|
||||
self.range.add(i)
|
||||
i += 1
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
def __contains__(self, val):
|
||||
return val in self.range
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.range_str, other.range_str)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.range_str == other.range_str
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.range_str != other.range_str
|
||||
|
||||
class UDPAsyncDNSHandler(object):
|
||||
dns_cache = lru_cache.LRUCache(timeout=1800)
|
||||
def __init__(self, params):
|
||||
self.params = params
|
||||
self.remote_addr = None
|
||||
self.call_back = None
|
||||
|
||||
def resolve(self, dns_resolver, remote_addr, call_back):
|
||||
if remote_addr in UDPAsyncDNSHandler.dns_cache:
|
||||
if call_back:
|
||||
call_back("", remote_addr, UDPAsyncDNSHandler.dns_cache[remote_addr], self.params)
|
||||
else:
|
||||
self.call_back = call_back
|
||||
self.remote_addr = remote_addr
|
||||
dns_resolver.resolve(remote_addr[0], self._handle_dns_resolved)
|
||||
UDPAsyncDNSHandler.dns_cache.sweep()
|
||||
|
||||
def _handle_dns_resolved(self, result, error):
|
||||
if error:
|
||||
logging.error("%s when resolve DNS" % (error,)) #drop
|
||||
return self.call_back(error, self.remote_addr, None, self.params)
|
||||
if result:
|
||||
ip = result[1]
|
||||
if ip:
|
||||
return self.call_back("", self.remote_addr, ip, self.params)
|
||||
logging.warning("can't resolve %s" % (self.remote_addr,))
|
||||
return self.call_back("fail to resolve", self.remote_addr, None, self.params)
|
||||
|
||||
def test_inet_conv():
|
||||
ipv4 = b'8.8.4.4'
|
||||
b = inet_pton(socket.AF_INET, ipv4)
|
||||
assert inet_ntop(socket.AF_INET, b) == ipv4
|
||||
ipv6 = b'2404:6800:4005:805::1011'
|
||||
b = inet_pton(socket.AF_INET6, ipv6)
|
||||
assert inet_ntop(socket.AF_INET6, b) == ipv6
|
||||
|
||||
|
||||
def test_parse_header():
|
||||
assert parse_header(b'\x03\x0ewww.google.com\x00\x50') == \
|
||||
(0, b'www.google.com', 80, 18)
|
||||
assert parse_header(b'\x01\x08\x08\x08\x08\x00\x35') == \
|
||||
(0, b'8.8.8.8', 53, 7)
|
||||
assert parse_header((b'\x04$\x04h\x00@\x05\x08\x05\x00\x00\x00\x00\x00'
|
||||
b'\x00\x10\x11\x00\x50')) == \
|
||||
(0, b'2404:6800:4005:805::1011', 80, 19)
|
||||
|
||||
|
||||
def test_pack_header():
|
||||
assert pack_addr(b'8.8.8.8') == b'\x01\x08\x08\x08\x08'
|
||||
assert pack_addr(b'2404:6800:4005:805::1011') == \
|
||||
b'\x04$\x04h\x00@\x05\x08\x05\x00\x00\x00\x00\x00\x00\x10\x11'
|
||||
assert pack_addr(b'www.google.com') == b'\x03\x0ewww.google.com'
|
||||
|
||||
|
||||
def test_ip_network():
|
||||
ip_network = IPNetwork('127.0.0.0/24,::ff:1/112,::1,192.168.1.1,192.0.2.0')
|
||||
assert '127.0.0.1' in ip_network
|
||||
assert '127.0.1.1' not in ip_network
|
||||
assert ':ff:ffff' in ip_network
|
||||
assert '::ffff:1' not in ip_network
|
||||
assert '::1' in ip_network
|
||||
assert '::2' not in ip_network
|
||||
assert '192.168.1.1' in ip_network
|
||||
assert '192.168.1.2' not in ip_network
|
||||
assert '192.0.2.1' in ip_network
|
||||
assert '192.0.3.1' in ip_network # 192.0.2.0 is treated as 192.0.2.0/23
|
||||
assert 'www.google.com' not in ip_network
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_inet_conv()
|
||||
test_parse_header()
|
||||
test_pack_header()
|
||||
test_ip_network()
|
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 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
|
@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import logging
|
||||
from ctypes import CDLL, c_char_p, c_int, c_ulonglong, byref, \
|
||||
create_string_buffer, c_void_p
|
||||
|
||||
__all__ = ['ciphers']
|
||||
|
||||
libsodium = None
|
||||
loaded = False
|
||||
|
||||
buf_size = 2048
|
||||
|
||||
# for salsa20 and chacha20
|
||||
BLOCK_SIZE = 64
|
||||
|
||||
|
||||
def load_libsodium():
|
||||
global loaded, libsodium, buf
|
||||
|
||||
from ctypes.util import find_library
|
||||
for p in ('sodium',):
|
||||
libsodium_path = find_library(p)
|
||||
if libsodium_path:
|
||||
break
|
||||
else:
|
||||
raise Exception('libsodium not found')
|
||||
logging.info('loading libsodium from %s', libsodium_path)
|
||||
libsodium = CDLL(libsodium_path)
|
||||
libsodium.sodium_init.restype = c_int
|
||||
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_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.sodium_init()
|
||||
|
||||
buf = create_string_buffer(buf_size)
|
||||
loaded = True
|
||||
|
||||
|
||||
class Salsa20Crypto(object):
|
||||
def __init__(self, cipher_name, key, iv, op):
|
||||
if not loaded:
|
||||
load_libsodium()
|
||||
self.key = key
|
||||
self.iv = iv
|
||||
self.key_ptr = c_char_p(key)
|
||||
self.iv_ptr = c_char_p(iv)
|
||||
if cipher_name == b'salsa20':
|
||||
self.cipher = libsodium.crypto_stream_salsa20_xor_ic
|
||||
elif cipher_name == b'chacha20':
|
||||
self.cipher = libsodium.crypto_stream_chacha20_xor_ic
|
||||
else:
|
||||
raise Exception('Unknown cipher')
|
||||
# byte counter, not block counter
|
||||
self.counter = 0
|
||||
|
||||
def update(self, data):
|
||||
global buf_size, buf
|
||||
l = len(data)
|
||||
|
||||
# we can only prepend some padding to make the encryption align to
|
||||
# blocks
|
||||
padding = self.counter % BLOCK_SIZE
|
||||
if buf_size < padding + l:
|
||||
buf_size = (padding + l) * 2
|
||||
buf = create_string_buffer(buf_size)
|
||||
|
||||
if padding:
|
||||
data = (b'\0' * padding) + data
|
||||
self.cipher(byref(buf), c_char_p(data), padding + l,
|
||||
self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr)
|
||||
self.counter += l
|
||||
# buf is copied to a str object when we access buf.raw
|
||||
# strip off the padding
|
||||
return buf.raw[padding:padding + l]
|
||||
|
||||
|
||||
ciphers = {
|
||||
b'salsa20': (32, 8, Salsa20Crypto),
|
||||
b'chacha20': (32, 8, Salsa20Crypto),
|
||||
}
|
||||
|
||||
|
||||
def test_salsa20():
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 1)
|
||||
decipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
def test_chacha20():
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 1)
|
||||
decipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_chacha20()
|
||||
test_salsa20()
|
@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import logging
|
||||
from ctypes import CDLL, c_char_p, c_int, c_long, byref,\
|
||||
create_string_buffer, c_void_p
|
||||
|
||||
__all__ = ['ciphers']
|
||||
|
||||
libcrypto = None
|
||||
loaded = False
|
||||
|
||||
buf_size = 2048
|
||||
|
||||
|
||||
def load_openssl():
|
||||
global loaded, libcrypto, buf
|
||||
|
||||
from ctypes.util import find_library
|
||||
for p in ('crypto', 'eay32', 'libeay32'):
|
||||
libcrypto_path = find_library(p)
|
||||
if libcrypto_path:
|
||||
break
|
||||
else:
|
||||
raise Exception('libcrypto(OpenSSL) not found')
|
||||
logging.info('loading libcrypto from %s', libcrypto_path)
|
||||
libcrypto = CDLL(libcrypto_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_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p,
|
||||
c_char_p, c_int)
|
||||
|
||||
libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
|
||||
libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,)
|
||||
if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'):
|
||||
libcrypto.OpenSSL_add_all_ciphers()
|
||||
|
||||
buf = create_string_buffer(buf_size)
|
||||
loaded = True
|
||||
|
||||
|
||||
def load_cipher(cipher_name):
|
||||
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)
|
||||
if cipher:
|
||||
cipher.restype = c_void_p
|
||||
return cipher()
|
||||
return None
|
||||
|
||||
|
||||
class CtypesCrypto(object):
|
||||
def __init__(self, cipher_name, key, iv, op):
|
||||
if not loaded:
|
||||
load_openssl()
|
||||
self._ctx = None
|
||||
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()
|
||||
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 update(self, data):
|
||||
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)
|
||||
# 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:
|
||||
libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx)
|
||||
libcrypto.EVP_CIPHER_CTX_free(self._ctx)
|
||||
|
||||
|
||||
ciphers = {
|
||||
b'aes-128-cfb': (16, 16, CtypesCrypto),
|
||||
b'aes-192-cfb': (24, 16, CtypesCrypto),
|
||||
b'aes-256-cfb': (32, 16, CtypesCrypto),
|
||||
b'aes-128-ofb': (16, 16, CtypesCrypto),
|
||||
b'aes-192-ofb': (24, 16, CtypesCrypto),
|
||||
b'aes-256-ofb': (32, 16, CtypesCrypto),
|
||||
b'aes-128-ctr': (16, 16, CtypesCrypto),
|
||||
b'aes-192-ctr': (24, 16, CtypesCrypto),
|
||||
b'aes-256-ctr': (32, 16, CtypesCrypto),
|
||||
b'aes-128-cfb8': (16, 16, CtypesCrypto),
|
||||
b'aes-192-cfb8': (24, 16, CtypesCrypto),
|
||||
b'aes-256-cfb8': (32, 16, CtypesCrypto),
|
||||
b'aes-128-cfb1': (16, 16, CtypesCrypto),
|
||||
b'aes-192-cfb1': (24, 16, CtypesCrypto),
|
||||
b'aes-256-cfb1': (32, 16, CtypesCrypto),
|
||||
b'bf-cfb': (16, 8, CtypesCrypto),
|
||||
b'camellia-128-cfb': (16, 16, CtypesCrypto),
|
||||
b'camellia-192-cfb': (24, 16, CtypesCrypto),
|
||||
b'camellia-256-cfb': (32, 16, CtypesCrypto),
|
||||
b'cast5-cfb': (16, 8, CtypesCrypto),
|
||||
b'des-cfb': (8, 8, CtypesCrypto),
|
||||
b'idea-cfb': (16, 8, CtypesCrypto),
|
||||
b'rc2-cfb': (16, 8, CtypesCrypto),
|
||||
b'rc4': (16, 0, CtypesCrypto),
|
||||
b'seed-cfb': (16, 16, CtypesCrypto),
|
||||
}
|
||||
|
||||
|
||||
def run_method(method):
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 1)
|
||||
decipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
def test_aes_128_cfb():
|
||||
run_method(b'aes-128-cfb')
|
||||
|
||||
|
||||
def test_aes_256_cfb():
|
||||
run_method(b'aes-256-cfb')
|
||||
|
||||
|
||||
def test_aes_128_cfb8():
|
||||
run_method(b'aes-128-cfb8')
|
||||
|
||||
|
||||
def test_aes_256_ofb():
|
||||
run_method(b'aes-256-ofb')
|
||||
|
||||
|
||||
def test_aes_256_ctr():
|
||||
run_method(b'aes-256-ctr')
|
||||
|
||||
|
||||
def test_bf_cfb():
|
||||
run_method(b'bf-cfb')
|
||||
|
||||
|
||||
def test_rc4():
|
||||
run_method(b'rc4')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_aes_128_cfb()
|
@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 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
|
||||
|
||||
from ctypes import c_char_p, c_int, c_long, byref,\
|
||||
create_string_buffer, c_void_p
|
||||
|
||||
from shadowsocks import common
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
__all__ = ['ciphers']
|
||||
|
||||
libcrypto = None
|
||||
loaded = False
|
||||
|
||||
buf_size = 2048
|
||||
|
||||
|
||||
def load_openssl():
|
||||
global loaded, libcrypto, buf
|
||||
|
||||
libcrypto = util.find_library(('crypto', 'eay32'),
|
||||
'EVP_get_cipherbyname',
|
||||
'libcrypto')
|
||||
if libcrypto is None:
|
||||
raise Exception('libcrypto(OpenSSL) not found')
|
||||
|
||||
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_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p,
|
||||
c_char_p, c_int)
|
||||
|
||||
if hasattr(libcrypto, "EVP_CIPHER_CTX_cleanup"):
|
||||
libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
|
||||
else:
|
||||
libcrypto.EVP_CIPHER_CTX_reset.argtypes = (c_void_p,)
|
||||
libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,)
|
||||
|
||||
libcrypto.RAND_bytes.restype = c_int
|
||||
libcrypto.RAND_bytes.argtypes = (c_void_p, c_int)
|
||||
|
||||
if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'):
|
||||
libcrypto.OpenSSL_add_all_ciphers()
|
||||
|
||||
buf = create_string_buffer(buf_size)
|
||||
loaded = True
|
||||
|
||||
|
||||
def load_cipher(cipher_name):
|
||||
func_name = 'EVP_' + cipher_name.replace('-', '_')
|
||||
cipher = getattr(libcrypto, func_name, None)
|
||||
if cipher:
|
||||
cipher.restype = c_void_p
|
||||
return cipher()
|
||||
return None
|
||||
|
||||
def rand_bytes(length):
|
||||
if not loaded:
|
||||
load_openssl()
|
||||
buf = create_string_buffer(length)
|
||||
r = libcrypto.RAND_bytes(buf, length)
|
||||
if r <= 0:
|
||||
raise Exception('RAND_bytes return error')
|
||||
return buf.raw
|
||||
|
||||
class OpenSSLCrypto(object):
|
||||
def __init__(self, cipher_name, key, iv, op):
|
||||
self._ctx = None
|
||||
if not loaded:
|
||||
load_openssl()
|
||||
cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(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()
|
||||
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 update(self, data):
|
||||
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)
|
||||
# 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:
|
||||
if hasattr(libcrypto, "EVP_CIPHER_CTX_cleanup"):
|
||||
libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx)
|
||||
else:
|
||||
libcrypto.EVP_CIPHER_CTX_reset(self._ctx)
|
||||
libcrypto.EVP_CIPHER_CTX_free(self._ctx)
|
||||
|
||||
|
||||
ciphers = {
|
||||
'aes-128-cbc': (16, 16, OpenSSLCrypto),
|
||||
'aes-192-cbc': (24, 16, OpenSSLCrypto),
|
||||
'aes-256-cbc': (32, 16, OpenSSLCrypto),
|
||||
'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),
|
||||
}
|
||||
|
||||
|
||||
def run_method(method):
|
||||
|
||||
cipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 1)
|
||||
decipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
def test_aes_128_cfb():
|
||||
run_method('aes-128-cfb')
|
||||
|
||||
|
||||
def test_aes_256_cfb():
|
||||
run_method('aes-256-cfb')
|
||||
|
||||
|
||||
def test_aes_128_cfb8():
|
||||
run_method('aes-128-cfb8')
|
||||
|
||||
|
||||
def test_aes_256_ofb():
|
||||
run_method('aes-256-ofb')
|
||||
|
||||
|
||||
def test_aes_256_ctr():
|
||||
run_method('aes-256-ctr')
|
||||
|
||||
|
||||
def test_bf_cfb():
|
||||
run_method('bf-cfb')
|
||||
|
||||
|
||||
def test_rc4():
|
||||
run_method('rc4')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_aes_128_cfb()
|
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 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 hashlib
|
||||
|
||||
from shadowsocks.crypto import openssl
|
||||
|
||||
__all__ = ['ciphers']
|
||||
|
||||
|
||||
def create_cipher(alg, key, iv, op, 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)
|
||||
|
||||
|
||||
ciphers = {
|
||||
'rc4-md5': (16, 16, create_cipher),
|
||||
'rc4-md5-6': (16, 6, create_cipher),
|
||||
}
|
||||
|
||||
|
||||
def test():
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 1)
|
||||
decipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 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
|
||||
|
||||
from ctypes import c_char_p, c_int, c_ulong, c_ulonglong, byref, \
|
||||
create_string_buffer, c_void_p
|
||||
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
__all__ = ['ciphers']
|
||||
|
||||
libsodium = None
|
||||
loaded = False
|
||||
|
||||
buf_size = 2048
|
||||
|
||||
# for salsa20 and chacha20 and chacha20-ietf
|
||||
BLOCK_SIZE = 64
|
||||
|
||||
|
||||
def load_libsodium():
|
||||
global loaded, libsodium, buf
|
||||
|
||||
libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic',
|
||||
'libsodium')
|
||||
if libsodium is None:
|
||||
raise Exception('libsodium not found')
|
||||
|
||||
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_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)
|
||||
|
||||
try:
|
||||
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)
|
||||
except:
|
||||
pass
|
||||
|
||||
buf = create_string_buffer(buf_size)
|
||||
loaded = True
|
||||
|
||||
|
||||
class SodiumCrypto(object):
|
||||
def __init__(self, cipher_name, key, iv, op):
|
||||
if not loaded:
|
||||
load_libsodium()
|
||||
self.key = key
|
||||
self.iv = iv
|
||||
self.key_ptr = c_char_p(key)
|
||||
self.iv_ptr = c_char_p(iv)
|
||||
if cipher_name == 'salsa20':
|
||||
self.cipher = libsodium.crypto_stream_salsa20_xor_ic
|
||||
elif cipher_name == 'chacha20':
|
||||
self.cipher = libsodium.crypto_stream_chacha20_xor_ic
|
||||
elif cipher_name == 'chacha20-ietf':
|
||||
self.cipher = libsodium.crypto_stream_chacha20_ietf_xor_ic
|
||||
else:
|
||||
raise Exception('Unknown cipher')
|
||||
# byte counter, not block counter
|
||||
self.counter = 0
|
||||
|
||||
def update(self, data):
|
||||
global buf_size, buf
|
||||
l = len(data)
|
||||
|
||||
# we can only prepend some padding to make the encryption align to
|
||||
# blocks
|
||||
padding = self.counter % BLOCK_SIZE
|
||||
if buf_size < padding + l:
|
||||
buf_size = (padding + l) * 2
|
||||
buf = create_string_buffer(buf_size)
|
||||
|
||||
if padding:
|
||||
data = (b'\0' * padding) + data
|
||||
self.cipher(byref(buf), c_char_p(data), padding + l,
|
||||
self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr)
|
||||
self.counter += l
|
||||
# buf is copied to a str object when we access buf.raw
|
||||
# strip off the padding
|
||||
return buf.raw[padding:padding + l]
|
||||
|
||||
|
||||
ciphers = {
|
||||
'salsa20': (32, 8, SodiumCrypto),
|
||||
'chacha20': (32, 8, SodiumCrypto),
|
||||
'chacha20-ietf': (32, 12, SodiumCrypto),
|
||||
}
|
||||
|
||||
|
||||
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():
|
||||
|
||||
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():
|
||||
|
||||
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)
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_chacha20_ietf()
|
||||
test_chacha20()
|
||||
test_salsa20()
|
@ -0,0 +1,181 @@
|
||||
# !/usr/bin/env python3
|
||||
#
|
||||
# Copyright 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 string
|
||||
import struct
|
||||
import hashlib
|
||||
|
||||
|
||||
__all__ = ['ciphers']
|
||||
|
||||
cached_tables = {}
|
||||
|
||||
if hasattr(string, 'maketrans'):
|
||||
maketrans = string.maketrans
|
||||
translate = string.translate
|
||||
else:
|
||||
maketrans = bytes.maketrans
|
||||
translate = bytes.translate
|
||||
|
||||
|
||||
def get_table(key):
|
||||
m = hashlib.md5()
|
||||
m.update(key)
|
||||
s = m.digest()
|
||||
a, b = struct.unpack('<QQ', s)
|
||||
table = maketrans(b'', b'')
|
||||
table = [table[i: i + 1] for i in range(len(table))]
|
||||
for i in range(1, 1024):
|
||||
table.sort(key=lambda x: int(a % (ord(x) + i)))
|
||||
return table
|
||||
|
||||
|
||||
def init_table(key):
|
||||
if key not in cached_tables:
|
||||
encrypt_table = b''.join(get_table(key))
|
||||
decrypt_table = maketrans(encrypt_table, maketrans(b'', b''))
|
||||
cached_tables[key] = [encrypt_table, decrypt_table]
|
||||
return cached_tables[key]
|
||||
|
||||
|
||||
class TableCipher(object):
|
||||
def __init__(self, cipher_name, key, iv, op):
|
||||
self._encrypt_table, self._decrypt_table = init_table(key)
|
||||
self._op = op
|
||||
|
||||
def update(self, data):
|
||||
if self._op:
|
||||
return translate(data, self._encrypt_table)
|
||||
else:
|
||||
return translate(data, self._decrypt_table)
|
||||
|
||||
class NoneCipher(object):
|
||||
def __init__(self, cipher_name, key, iv, op):
|
||||
pass
|
||||
|
||||
def update(self, data):
|
||||
return data
|
||||
|
||||
ciphers = {
|
||||
'none': (16, 0, NoneCipher),
|
||||
'table': (16, 0, TableCipher)
|
||||
}
|
||||
|
||||
|
||||
def test_table_result():
|
||||
from shadowsocks.common import ord
|
||||
target1 = [
|
||||
[60, 53, 84, 138, 217, 94, 88, 23, 39, 242, 219, 35, 12, 157, 165, 181,
|
||||
255, 143, 83, 247, 162, 16, 31, 209, 190, 171, 115, 65, 38, 41, 21,
|
||||
245, 236, 46, 121, 62, 166, 233, 44, 154, 153, 145, 230, 49, 128, 216,
|
||||
173, 29, 241, 119, 64, 229, 194, 103, 131, 110, 26, 197, 218, 59, 204,
|
||||
56, 27, 34, 141, 221, 149, 239, 192, 195, 24, 155, 170, 183, 11, 254,
|
||||
213, 37, 137, 226, 75, 203, 55, 19, 72, 248, 22, 129, 33, 175, 178,
|
||||
10, 198, 71, 77, 36, 113, 167, 48, 2, 117, 140, 142, 66, 199, 232,
|
||||
243, 32, 123, 54, 51, 82, 57, 177, 87, 251, 150, 196, 133, 5, 253,
|
||||
130, 8, 184, 14, 152, 231, 3, 186, 159, 76, 89, 228, 205, 156, 96,
|
||||
163, 146, 18, 91, 132, 85, 80, 109, 172, 176, 105, 13, 50, 235, 127,
|
||||
0, 189, 95, 98, 136, 250, 200, 108, 179, 211, 214, 106, 168, 78, 79,
|
||||
74, 210, 30, 73, 201, 151, 208, 114, 101, 174, 92, 52, 120, 240, 15,
|
||||
169, 220, 182, 81, 224, 43, 185, 40, 99, 180, 17, 212, 158, 42, 90, 9,
|
||||
191, 45, 6, 25, 4, 222, 67, 126, 1, 116, 124, 206, 69, 61, 7, 68, 97,
|
||||
202, 63, 244, 20, 28, 58, 93, 134, 104, 144, 227, 147, 102, 118, 135,
|
||||
148, 47, 238, 86, 112, 122, 70, 107, 215, 100, 139, 223, 225, 164,
|
||||
237, 111, 125, 207, 160, 187, 246, 234, 161, 188, 193, 249, 252],
|
||||
[151, 205, 99, 127, 201, 119, 199, 211, 122, 196, 91, 74, 12, 147, 124,
|
||||
180, 21, 191, 138, 83, 217, 30, 86, 7, 70, 200, 56, 62, 218, 47, 168,
|
||||
22, 107, 88, 63, 11, 95, 77, 28, 8, 188, 29, 194, 186, 38, 198, 33,
|
||||
230, 98, 43, 148, 110, 177, 1, 109, 82, 61, 112, 219, 59, 0, 210, 35,
|
||||
215, 50, 27, 103, 203, 212, 209, 235, 93, 84, 169, 166, 80, 130, 94,
|
||||
164, 165, 142, 184, 111, 18, 2, 141, 232, 114, 6, 131, 195, 139, 176,
|
||||
220, 5, 153, 135, 213, 154, 189, 238, 174, 226, 53, 222, 146, 162,
|
||||
236, 158, 143, 55, 244, 233, 96, 173, 26, 206, 100, 227, 49, 178, 34,
|
||||
234, 108, 207, 245, 204, 150, 44, 87, 121, 54, 140, 118, 221, 228,
|
||||
155, 78, 3, 239, 101, 64, 102, 17, 223, 41, 137, 225, 229, 66, 116,
|
||||
171, 125, 40, 39, 71, 134, 13, 193, 129, 247, 251, 20, 136, 242, 14,
|
||||
36, 97, 163, 181, 72, 25, 144, 46, 175, 89, 145, 113, 90, 159, 190,
|
||||
15, 183, 73, 123, 187, 128, 248, 252, 152, 24, 197, 68, 253, 52, 69,
|
||||
117, 57, 92, 104, 157, 170, 214, 81, 60, 133, 208, 246, 172, 23, 167,
|
||||
160, 192, 76, 161, 237, 45, 4, 58, 10, 182, 65, 202, 240, 185, 241,
|
||||
79, 224, 132, 51, 42, 126, 105, 37, 250, 149, 32, 243, 231, 67, 179,
|
||||
48, 9, 106, 216, 31, 249, 19, 85, 254, 156, 115, 255, 120, 75, 16]]
|
||||
|
||||
target2 = [
|
||||
[124, 30, 170, 247, 27, 127, 224, 59, 13, 22, 196, 76, 72, 154, 32,
|
||||
209, 4, 2, 131, 62, 101, 51, 230, 9, 166, 11, 99, 80, 208, 112, 36,
|
||||
248, 81, 102, 130, 88, 218, 38, 168, 15, 241, 228, 167, 117, 158, 41,
|
||||
10, 180, 194, 50, 204, 243, 246, 251, 29, 198, 219, 210, 195, 21, 54,
|
||||
91, 203, 221, 70, 57, 183, 17, 147, 49, 133, 65, 77, 55, 202, 122,
|
||||
162, 169, 188, 200, 190, 125, 63, 244, 96, 31, 107, 106, 74, 143, 116,
|
||||
148, 78, 46, 1, 137, 150, 110, 181, 56, 95, 139, 58, 3, 231, 66, 165,
|
||||
142, 242, 43, 192, 157, 89, 175, 109, 220, 128, 0, 178, 42, 255, 20,
|
||||
214, 185, 83, 160, 253, 7, 23, 92, 111, 153, 26, 226, 33, 176, 144,
|
||||
18, 216, 212, 28, 151, 71, 206, 222, 182, 8, 174, 205, 201, 152, 240,
|
||||
155, 108, 223, 104, 239, 98, 164, 211, 184, 34, 193, 14, 114, 187, 40,
|
||||
254, 12, 67, 93, 217, 6, 94, 16, 19, 82, 86, 245, 24, 197, 134, 132,
|
||||
138, 229, 121, 5, 235, 238, 85, 47, 103, 113, 179, 69, 250, 45, 135,
|
||||
156, 25, 61, 75, 44, 146, 189, 84, 207, 172, 119, 53, 123, 186, 120,
|
||||
171, 68, 227, 145, 136, 100, 90, 48, 79, 159, 149, 39, 213, 236, 126,
|
||||
52, 60, 225, 199, 105, 73, 233, 252, 118, 215, 35, 115, 64, 37, 97,
|
||||
129, 161, 177, 87, 237, 141, 173, 191, 163, 140, 234, 232, 249],
|
||||
[117, 94, 17, 103, 16, 186, 172, 127, 146, 23, 46, 25, 168, 8, 163, 39,
|
||||
174, 67, 137, 175, 121, 59, 9, 128, 179, 199, 132, 4, 140, 54, 1, 85,
|
||||
14, 134, 161, 238, 30, 241, 37, 224, 166, 45, 119, 109, 202, 196, 93,
|
||||
190, 220, 69, 49, 21, 228, 209, 60, 73, 99, 65, 102, 7, 229, 200, 19,
|
||||
82, 240, 71, 105, 169, 214, 194, 64, 142, 12, 233, 88, 201, 11, 72,
|
||||
92, 221, 27, 32, 176, 124, 205, 189, 177, 246, 35, 112, 219, 61, 129,
|
||||
170, 173, 100, 84, 242, 157, 26, 218, 20, 33, 191, 155, 232, 87, 86,
|
||||
153, 114, 97, 130, 29, 192, 164, 239, 90, 43, 236, 208, 212, 185, 75,
|
||||
210, 0, 81, 227, 5, 116, 243, 34, 18, 182, 70, 181, 197, 217, 95, 183,
|
||||
101, 252, 248, 107, 89, 136, 216, 203, 68, 91, 223, 96, 141, 150, 131,
|
||||
13, 152, 198, 111, 44, 222, 125, 244, 76, 251, 158, 106, 24, 42, 38,
|
||||
77, 2, 213, 207, 249, 147, 113, 135, 245, 118, 193, 47, 98, 145, 66,
|
||||
160, 123, 211, 165, 78, 204, 80, 250, 110, 162, 48, 58, 10, 180, 55,
|
||||
231, 79, 149, 74, 62, 50, 148, 143, 206, 28, 15, 57, 159, 139, 225,
|
||||
122, 237, 138, 171, 36, 56, 115, 63, 144, 154, 6, 230, 133, 215, 41,
|
||||
184, 22, 104, 254, 234, 253, 187, 226, 247, 188, 156, 151, 40, 108,
|
||||
51, 83, 178, 52, 3, 31, 255, 195, 53, 235, 126, 167, 120]]
|
||||
|
||||
encrypt_table = b''.join(get_table(b'foobar!'))
|
||||
decrypt_table = maketrans(encrypt_table, maketrans(b'', b''))
|
||||
|
||||
for i in range(0, 256):
|
||||
assert (target1[0][i] == ord(encrypt_table[i]))
|
||||
assert (target1[1][i] == ord(decrypt_table[i]))
|
||||
|
||||
encrypt_table = b''.join(get_table(b'barfoo!'))
|
||||
decrypt_table = maketrans(encrypt_table, maketrans(b'', b''))
|
||||
|
||||
for i in range(0, 256):
|
||||
assert (target2[0][i] == ord(encrypt_table[i]))
|
||||
assert (target2[1][i] == ord(decrypt_table[i]))
|
||||
|
||||
|
||||
def test_encryption():
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = TableCipher('table', b'test', b'', 1)
|
||||
decipher = TableCipher('table', b'test', b'', 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_table_result()
|
||||
test_encryption()
|
@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 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 os
|
||||
import logging
|
||||
|
||||
|
||||
def find_library_nt(name):
|
||||
# modified from ctypes.util
|
||||
# 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
|
||||
results = []
|
||||
for directory in os.environ['PATH'].split(os.pathsep):
|
||||
fname = os.path.join(directory, name)
|
||||
if os.path.isfile(fname):
|
||||
results.append(fname)
|
||||
if fname.lower().endswith(".dll"):
|
||||
continue
|
||||
fname = fname + ".dll"
|
||||
if os.path.isfile(fname):
|
||||
results.append(fname)
|
||||
return results
|
||||
|
||||
|
||||
def find_library(possible_lib_names, search_symbol, library_name):
|
||||
import ctypes.util
|
||||
from ctypes import CDLL
|
||||
|
||||
paths = []
|
||||
|
||||
if type(possible_lib_names) not in (list, tuple):
|
||||
possible_lib_names = [possible_lib_names]
|
||||
|
||||
lib_names = []
|
||||
for lib_name in possible_lib_names:
|
||||
lib_names.append(lib_name)
|
||||
lib_names.append('lib' + lib_name)
|
||||
|
||||
for name in lib_names:
|
||||
if os.name == "nt":
|
||||
paths.extend(find_library_nt(name))
|
||||
else:
|
||||
path = ctypes.util.find_library(name)
|
||||
if path:
|
||||
paths.append(path)
|
||||
|
||||
if not paths:
|
||||
# We may get here when find_library fails because, for example,
|
||||
# the user does not have sufficient privileges to access those
|
||||
# tools underlying find_library on linux.
|
||||
import glob
|
||||
|
||||
for name in lib_names:
|
||||
patterns = [
|
||||
'/usr/local/lib*/lib%s.*' % name,
|
||||
'/usr/lib*/lib%s.*' % name,
|
||||
'lib%s.*' % name,
|
||||
'%s.dll' % name]
|
||||
|
||||
for pat in patterns:
|
||||
files = glob.glob(pat)
|
||||
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:
|
||||
if path == paths[-1]:
|
||||
raise
|
||||
return None
|
||||
|
||||
|
||||
def run_cipher(cipher, decipher):
|
||||
from os import urandom
|
||||
import random
|
||||
import time
|
||||
|
||||
BLOCK_SIZE = 16384
|
||||
rounds = 1 * 1024
|
||||
plain = urandom(BLOCK_SIZE * rounds)
|
||||
|
||||
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)
|
||||
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]))
|
||||
pos += l
|
||||
end = time.time()
|
||||
print('speed: %d bytes/s' % (BLOCK_SIZE * rounds / (end - start)))
|
||||
assert b''.join(results) == plain
|
||||
|
||||
|
||||
def test_find_library():
|
||||
assert find_library('c', 'strcpy', 'libc') is not None
|
||||
assert find_library(['c'], 'strcpy', 'libc') is not None
|
||||
assert find_library(('c',), 'strcpy', 'libc') is not None
|
||||
assert find_library(('crypto', 'eay32'), 'EVP_CipherUpdate',
|
||||
'libcrypto') is not None
|
||||
assert find_library('notexist', 'strcpy', 'libnotexist') is None
|
||||
assert find_library('c', 'symbol_not_exist', 'c') is None
|
||||
assert find_library(('notexist', 'c', 'crypto', 'eay32'),
|
||||
'EVP_CipherUpdate', 'libc') is not None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_find_library()
|
@ -0,0 +1,208 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014-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 os
|
||||
import sys
|
||||
import logging
|
||||
import signal
|
||||
import time
|
||||
from shadowsocks import common, shell
|
||||
|
||||
# this module is ported from ShadowVPN daemon.c
|
||||
|
||||
|
||||
def daemon_exec(config):
|
||||
if 'daemon' in config:
|
||||
if os.name != 'posix':
|
||||
raise Exception('daemon mode is only supported on Unix')
|
||||
command = config['daemon']
|
||||
if not command:
|
||||
command = 'start'
|
||||
pid_file = config['pid-file']
|
||||
log_file = config['log-file']
|
||||
if command == 'start':
|
||||
daemon_start(pid_file, log_file)
|
||||
elif command == 'stop':
|
||||
daemon_stop(pid_file)
|
||||
# always exit after daemon_stop
|
||||
sys.exit(0)
|
||||
elif command == 'restart':
|
||||
daemon_stop(pid_file)
|
||||
daemon_start(pid_file, log_file)
|
||||
else:
|
||||
raise Exception('unsupported daemon command %s' % command)
|
||||
|
||||
|
||||
def write_pid_file(pid_file, pid):
|
||||
import fcntl
|
||||
import stat
|
||||
|
||||
try:
|
||||
fd = os.open(pid_file, os.O_RDWR | os.O_CREAT,
|
||||
stat.S_IRUSR | stat.S_IWUSR)
|
||||
except OSError as e:
|
||||
shell.print_exception(e)
|
||||
return -1
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
|
||||
assert flags != -1
|
||||
flags |= fcntl.FD_CLOEXEC
|
||||
r = fcntl.fcntl(fd, fcntl.F_SETFD, flags)
|
||||
assert r != -1
|
||||
# There is no platform independent way to implement fcntl(fd, F_SETLK, &fl)
|
||||
# via fcntl.fcntl. So use lockf instead
|
||||
try:
|
||||
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB, 0, 0, os.SEEK_SET)
|
||||
except IOError:
|
||||
r = os.read(fd, 32)
|
||||
if r:
|
||||
logging.error('already started at pid %s' % common.to_str(r))
|
||||
else:
|
||||
logging.error('already started')
|
||||
os.close(fd)
|
||||
return -1
|
||||
os.ftruncate(fd, 0)
|
||||
os.write(fd, common.to_bytes(str(pid)))
|
||||
return 0
|
||||
|
||||
|
||||
def freopen(f, mode, stream):
|
||||
oldf = open(f, mode)
|
||||
oldfd = oldf.fileno()
|
||||
newfd = stream.fileno()
|
||||
os.close(newfd)
|
||||
os.dup2(oldfd, newfd)
|
||||
|
||||
|
||||
def daemon_start(pid_file, log_file):
|
||||
|
||||
def handle_exit(signum, _):
|
||||
if signum == signal.SIGTERM:
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
|
||||
signal.signal(signal.SIGINT, handle_exit)
|
||||
signal.signal(signal.SIGTERM, handle_exit)
|
||||
|
||||
# fork only once because we are sure parent will exit
|
||||
pid = os.fork()
|
||||
assert pid != -1
|
||||
|
||||
if pid > 0:
|
||||
# parent waits for its child
|
||||
time.sleep(5)
|
||||
sys.exit(0)
|
||||
|
||||
# child signals its parent to exit
|
||||
ppid = os.getppid()
|
||||
pid = os.getpid()
|
||||
if write_pid_file(pid_file, pid) != 0:
|
||||
os.kill(ppid, signal.SIGINT)
|
||||
sys.exit(1)
|
||||
|
||||
os.setsid()
|
||||
signal.signal(signal.SIG_IGN, signal.SIGHUP)
|
||||
|
||||
print('started')
|
||||
os.kill(ppid, signal.SIGTERM)
|
||||
|
||||
sys.stdin.close()
|
||||
try:
|
||||
freopen(log_file, 'a', sys.stdout)
|
||||
freopen(log_file, 'a', sys.stderr)
|
||||
except IOError as e:
|
||||
shell.print_exception(e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def daemon_stop(pid_file):
|
||||
import errno
|
||||
try:
|
||||
with open(pid_file) as f:
|
||||
buf = f.read()
|
||||
pid = common.to_str(buf)
|
||||
if not buf:
|
||||
logging.error('not running')
|
||||
except IOError as e:
|
||||
shell.print_exception(e)
|
||||
if e.errno == errno.ENOENT:
|
||||
# always exit 0 if we are sure daemon is not running
|
||||
logging.error('not running')
|
||||
return
|
||||
sys.exit(1)
|
||||
pid = int(pid)
|
||||
if pid > 0:
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ESRCH:
|
||||
logging.error('not running')
|
||||
# always exit 0 if we are sure daemon is not running
|
||||
return
|
||||
shell.print_exception(e)
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.error('pid is not positive: %d', pid)
|
||||
|
||||
# sleep for maximum 10s
|
||||
for i in range(0, 200):
|
||||
try:
|
||||
# query for the pid
|
||||
os.kill(pid, 0)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ESRCH:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
logging.error('timed out when stopping pid %d', pid)
|
||||
sys.exit(1)
|
||||
print('stopped')
|
||||
os.unlink(pid_file)
|
||||
|
||||
|
||||
def set_user(username):
|
||||
if username is None:
|
||||
return
|
||||
|
||||
import pwd
|
||||
import grp
|
||||
|
||||
try:
|
||||
pwrec = pwd.getpwnam(username)
|
||||
except KeyError:
|
||||
logging.error('user not found: %s' % username)
|
||||
raise
|
||||
user = pwrec[0]
|
||||
uid = pwrec[2]
|
||||
gid = pwrec[3]
|
||||
|
||||
cur_uid = os.getuid()
|
||||
if uid == cur_uid:
|
||||
return
|
||||
if cur_uid != 0:
|
||||
logging.error('can not set user as nonroot user')
|
||||
# will raise later
|
||||
|
||||
# inspired by supervisor
|
||||
if hasattr(os, 'setgroups'):
|
||||
groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]]
|
||||
groups.insert(0, gid)
|
||||
os.setgroups(groups)
|
||||
os.setgid(gid)
|
||||
os.setuid(uid)
|
@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from shadowsocks import common
|
||||
from shadowsocks.crypto import rc4_md5, openssl, sodium, table
|
||||
|
||||
|
||||
method_supported = {}
|
||||
method_supported.update(rc4_md5.ciphers)
|
||||
method_supported.update(openssl.ciphers)
|
||||
method_supported.update(sodium.ciphers)
|
||||
method_supported.update(table.ciphers)
|
||||
|
||||
|
||||
def random_string(length):
|
||||
try:
|
||||
return os.urandom(length)
|
||||
except NotImplementedError as e:
|
||||
return openssl.rand_bytes(length)
|
||||
|
||||
cached_keys = {}
|
||||
|
||||
|
||||
def try_cipher(key, method=None):
|
||||
Encryptor(key, method)
|
||||
|
||||
|
||||
def EVP_BytesToKey(password, key_len, iv_len):
|
||||
# equivalent to OpenSSL's EVP_BytesToKey() with count 1
|
||||
# so that we make the same key and iv as nodejs version
|
||||
if hasattr(password, 'encode'):
|
||||
password = password.encode('utf-8')
|
||||
cached_key = '%s-%d-%d' % (password, key_len, iv_len)
|
||||
r = cached_keys.get(cached_key, None)
|
||||
if r:
|
||||
return r
|
||||
m = []
|
||||
i = 0
|
||||
while len(b''.join(m)) < (key_len + iv_len):
|
||||
md5 = hashlib.md5()
|
||||
data = password
|
||||
if i > 0:
|
||||
data = m[i - 1] + password
|
||||
md5.update(data)
|
||||
m.append(md5.digest())
|
||||
i += 1
|
||||
ms = b''.join(m)
|
||||
key = ms[:key_len]
|
||||
iv = ms[key_len:key_len + iv_len]
|
||||
cached_keys[cached_key] = (key, iv)
|
||||
return key, iv
|
||||
|
||||
|
||||
class Encryptor(object):
|
||||
def __init__(self, key, method, iv = None):
|
||||
self.key = key
|
||||
self.method = method
|
||||
self.iv = None
|
||||
self.iv_sent = False
|
||||
self.cipher_iv = b''
|
||||
self.iv_buf = b''
|
||||
self.cipher_key = b''
|
||||
self.decipher = None
|
||||
method = method.lower()
|
||||
self._method_info = self.get_method_info(method)
|
||||
if self._method_info:
|
||||
if iv is None or len(iv) != self._method_info[1]:
|
||||
self.cipher = self.get_cipher(key, method, 1,
|
||||
random_string(self._method_info[1]))
|
||||
else:
|
||||
self.cipher = self.get_cipher(key, method, 1, iv)
|
||||
else:
|
||||
logging.error('method %s not supported' % method)
|
||||
sys.exit(1)
|
||||
|
||||
def get_method_info(self, method):
|
||||
method = method.lower()
|
||||
m = method_supported.get(method)
|
||||
return m
|
||||
|
||||
def iv_len(self):
|
||||
return len(self.cipher_iv)
|
||||
|
||||
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])
|
||||
else:
|
||||
# key_length == 0 indicates we should use the key directly
|
||||
key, iv = password, b''
|
||||
|
||||
iv = iv[:m[1]]
|
||||
if op == 1:
|
||||
# this iv is for cipher not decipher
|
||||
self.cipher_iv = iv[:m[1]]
|
||||
self.cipher_key = key
|
||||
return m[2](method, key, iv, op)
|
||||
|
||||
def encrypt(self, buf):
|
||||
if len(buf) == 0:
|
||||
return buf
|
||||
if self.iv_sent:
|
||||
return self.cipher.update(buf)
|
||||
else:
|
||||
self.iv_sent = True
|
||||
return self.cipher_iv + self.cipher.update(buf)
|
||||
|
||||
def decrypt(self, buf):
|
||||
if len(buf) == 0:
|
||||
return buf
|
||||
if self.decipher is not None: #optimize
|
||||
return self.decipher.update(buf)
|
||||
|
||||
decipher_iv_len = self._method_info[1]
|
||||
if len(self.iv_buf) <= decipher_iv_len:
|
||||
self.iv_buf += buf
|
||||
if len(self.iv_buf) > decipher_iv_len:
|
||||
decipher_iv = self.iv_buf[:decipher_iv_len]
|
||||
self.decipher = self.get_cipher(self.key, self.method, 0,
|
||||
iv=decipher_iv)
|
||||
buf = self.iv_buf[decipher_iv_len:]
|
||||
del self.iv_buf
|
||||
return self.decipher.update(buf)
|
||||
else:
|
||||
return b''
|
||||
|
||||
def encrypt_all(password, method, op, data):
|
||||
result = []
|
||||
method = method.lower()
|
||||
(key_len, iv_len, m) = method_supported[method]
|
||||
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))
|
||||
return b''.join(result)
|
||||
|
||||
def encrypt_key(password, method):
|
||||
method = method.lower()
|
||||
(key_len, iv_len, m) = method_supported[method]
|
||||
if key_len > 0:
|
||||
key, _ = EVP_BytesToKey(password, key_len, iv_len)
|
||||
else:
|
||||
key = password
|
||||
return key
|
||||
|
||||
def encrypt_iv_len(method):
|
||||
method = method.lower()
|
||||
(key_len, iv_len, m) = method_supported[method]
|
||||
return iv_len
|
||||
|
||||
def encrypt_new_iv(method):
|
||||
method = method.lower()
|
||||
(key_len, iv_len, m) = method_supported[method]
|
||||
return random_string(iv_len)
|
||||
|
||||
def encrypt_all_iv(key, method, op, data, ref_iv):
|
||||
result = []
|
||||
method = method.lower()
|
||||
(key_len, iv_len, m) = method_supported[method]
|
||||
if op:
|
||||
iv = ref_iv[0]
|
||||
result.append(iv)
|
||||
else:
|
||||
iv = data[:iv_len]
|
||||
data = data[iv_len:]
|
||||
ref_iv[0] = iv
|
||||
cipher = m(method, key, iv, op)
|
||||
result.append(cipher.update(data))
|
||||
return b''.join(result)
|
||||
|
||||
|
||||
CIPHERS_TO_TEST = [
|
||||
'aes-128-cfb',
|
||||
'aes-256-cfb',
|
||||
'rc4-md5',
|
||||
'salsa20',
|
||||
'chacha20',
|
||||
'table',
|
||||
]
|
||||
|
||||
|
||||
def test_encryptor():
|
||||
from os import urandom
|
||||
plain = urandom(10240)
|
||||
for method in CIPHERS_TO_TEST:
|
||||
logging.warn(method)
|
||||
encryptor = Encryptor(b'key', method)
|
||||
decryptor = Encryptor(b'key', method)
|
||||
cipher = encryptor.encrypt(plain)
|
||||
plain2 = decryptor.decrypt(cipher)
|
||||
assert plain == plain2
|
||||
|
||||
|
||||
def test_encrypt_all():
|
||||
from os import urandom
|
||||
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)
|
||||
assert plain == plain2
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_encrypt_all()
|
||||
test_encryptor()
|
@ -0,0 +1,258 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013-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 ssloop
|
||||
# https://github.com/clowwindy/ssloop
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import os
|
||||
import time
|
||||
import socket
|
||||
import select
|
||||
import errno
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from shadowsocks import shell
|
||||
|
||||
|
||||
__all__ = ['EventLoop', 'POLL_NULL', 'POLL_IN', 'POLL_OUT', 'POLL_ERR',
|
||||
'POLL_HUP', 'POLL_NVAL', 'EVENT_NAMES']
|
||||
|
||||
POLL_NULL = 0x00
|
||||
POLL_IN = 0x01
|
||||
POLL_OUT = 0x04
|
||||
POLL_ERR = 0x08
|
||||
POLL_HUP = 0x10
|
||||
POLL_NVAL = 0x20
|
||||
|
||||
|
||||
EVENT_NAMES = {
|
||||
POLL_NULL: 'POLL_NULL',
|
||||
POLL_IN: 'POLL_IN',
|
||||
POLL_OUT: 'POLL_OUT',
|
||||
POLL_ERR: 'POLL_ERR',
|
||||
POLL_HUP: 'POLL_HUP',
|
||||
POLL_NVAL: 'POLL_NVAL',
|
||||
}
|
||||
|
||||
# we check timeouts every TIMEOUT_PRECISION seconds
|
||||
TIMEOUT_PRECISION = 2
|
||||
|
||||
|
||||
class KqueueLoop(object):
|
||||
|
||||
MAX_EVENTS = 1024
|
||||
|
||||
def __init__(self):
|
||||
self._kqueue = select.kqueue()
|
||||
self._fds = {}
|
||||
|
||||
def _control(self, fd, mode, flags):
|
||||
events = []
|
||||
if mode & POLL_IN:
|
||||
events.append(select.kevent(fd, select.KQ_FILTER_READ, flags))
|
||||
if mode & POLL_OUT:
|
||||
events.append(select.kevent(fd, select.KQ_FILTER_WRITE, flags))
|
||||
for e in events:
|
||||
self._kqueue.control([e], 0)
|
||||
|
||||
def poll(self, timeout):
|
||||
if timeout < 0:
|
||||
timeout = None # kqueue behaviour
|
||||
events = self._kqueue.control(None, KqueueLoop.MAX_EVENTS, timeout)
|
||||
results = defaultdict(lambda: POLL_NULL)
|
||||
for e in events:
|
||||
fd = e.ident
|
||||
if e.filter == select.KQ_FILTER_READ:
|
||||
results[fd] |= POLL_IN
|
||||
elif e.filter == select.KQ_FILTER_WRITE:
|
||||
results[fd] |= POLL_OUT
|
||||
return results.items()
|
||||
|
||||
def register(self, fd, mode):
|
||||
self._fds[fd] = mode
|
||||
self._control(fd, mode, select.KQ_EV_ADD)
|
||||
|
||||
def unregister(self, fd):
|
||||
self._control(fd, self._fds[fd], select.KQ_EV_DELETE)
|
||||
del self._fds[fd]
|
||||
|
||||
def modify(self, fd, mode):
|
||||
self.unregister(fd)
|
||||
self.register(fd, mode)
|
||||
|
||||
def close(self):
|
||||
self._kqueue.close()
|
||||
|
||||
|
||||
class SelectLoop(object):
|
||||
|
||||
def __init__(self):
|
||||
self._r_list = set()
|
||||
self._w_list = set()
|
||||
self._x_list = set()
|
||||
|
||||
def poll(self, timeout):
|
||||
r, w, x = select.select(self._r_list, self._w_list, self._x_list,
|
||||
timeout)
|
||||
results = defaultdict(lambda: POLL_NULL)
|
||||
for p in [(r, POLL_IN), (w, POLL_OUT), (x, POLL_ERR)]:
|
||||
for fd in p[0]:
|
||||
results[fd] |= p[1]
|
||||
return results.items()
|
||||
|
||||
def register(self, fd, mode):
|
||||
if mode & POLL_IN:
|
||||
self._r_list.add(fd)
|
||||
if mode & POLL_OUT:
|
||||
self._w_list.add(fd)
|
||||
if mode & POLL_ERR:
|
||||
self._x_list.add(fd)
|
||||
|
||||
def unregister(self, fd):
|
||||
if fd in self._r_list:
|
||||
self._r_list.remove(fd)
|
||||
if fd in self._w_list:
|
||||
self._w_list.remove(fd)
|
||||
if fd in self._x_list:
|
||||
self._x_list.remove(fd)
|
||||
|
||||
def modify(self, fd, mode):
|
||||
self.unregister(fd)
|
||||
self.register(fd, mode)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class EventLoop(object):
|
||||
def __init__(self):
|
||||
if hasattr(select, 'epoll'):
|
||||
self._impl = select.epoll()
|
||||
model = 'epoll'
|
||||
elif hasattr(select, 'kqueue'):
|
||||
self._impl = KqueueLoop()
|
||||
model = 'kqueue'
|
||||
elif hasattr(select, 'select'):
|
||||
self._impl = SelectLoop()
|
||||
model = 'select'
|
||||
else:
|
||||
raise Exception('can not find any available functions in select '
|
||||
'package')
|
||||
self._fdmap = {} # (f, handler)
|
||||
self._last_time = time.time()
|
||||
self._periodic_callbacks = []
|
||||
self._stopping = False
|
||||
logging.debug('using event model: %s', model)
|
||||
|
||||
def poll(self, timeout=None):
|
||||
events = self._impl.poll(timeout)
|
||||
return [(self._fdmap[fd][0], fd, event) for fd, event in events]
|
||||
|
||||
def add(self, f, mode, handler):
|
||||
fd = f.fileno()
|
||||
self._fdmap[fd] = (f, handler)
|
||||
self._impl.register(fd, mode)
|
||||
|
||||
def remove(self, f):
|
||||
fd = f.fileno()
|
||||
del self._fdmap[fd]
|
||||
self._impl.unregister(fd)
|
||||
|
||||
def removefd(self, fd):
|
||||
del self._fdmap[fd]
|
||||
self._impl.unregister(fd)
|
||||
|
||||
def add_periodic(self, callback):
|
||||
self._periodic_callbacks.append(callback)
|
||||
|
||||
def remove_periodic(self, callback):
|
||||
self._periodic_callbacks.remove(callback)
|
||||
|
||||
def modify(self, f, mode):
|
||||
fd = f.fileno()
|
||||
self._impl.modify(fd, mode)
|
||||
|
||||
def stop(self):
|
||||
self._stopping = True
|
||||
|
||||
def run(self):
|
||||
events = []
|
||||
while not self._stopping:
|
||||
asap = False
|
||||
try:
|
||||
events = self.poll(TIMEOUT_PRECISION)
|
||||
except (OSError, IOError) as e:
|
||||
if errno_from_exception(e) in (errno.EPIPE, errno.EINTR):
|
||||
# EPIPE: Happens when the client closes the connection
|
||||
# EINTR: Happens when received a signal
|
||||
# handles them as soon as possible
|
||||
asap = True
|
||||
logging.debug('poll:%s', e)
|
||||
else:
|
||||
logging.error('poll:%s', e)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
handle = False
|
||||
for sock, fd, event in events:
|
||||
handler = self._fdmap.get(fd, None)
|
||||
if handler is not None:
|
||||
handler = handler[1]
|
||||
try:
|
||||
handle = handler.handle_event(sock, fd, event) or handle
|
||||
except (OSError, IOError) as e:
|
||||
shell.print_exception(e)
|
||||
now = time.time()
|
||||
if asap or now - self._last_time >= TIMEOUT_PRECISION:
|
||||
for callback in self._periodic_callbacks:
|
||||
callback()
|
||||
self._last_time = now
|
||||
if events and not handle:
|
||||
time.sleep(0.001)
|
||||
|
||||
def __del__(self):
|
||||
self._impl.close()
|
||||
|
||||
|
||||
# from tornado
|
||||
def errno_from_exception(e):
|
||||
"""Provides the errno from an Exception object.
|
||||
|
||||
There are cases that the errno attribute was not set so we pull
|
||||
the errno out of the args but if someone instatiates an Exception
|
||||
without any args you will get a tuple error. So this function
|
||||
abstracts all that behavior to give you a safe way to get the
|
||||
errno.
|
||||
"""
|
||||
|
||||
if hasattr(e, 'errno'):
|
||||
return e.errno
|
||||
elif e.args:
|
||||
return e.args[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
# from tornado
|
||||
def get_sock_error(sock):
|
||||
error_number = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
|
||||
return socket.error(error_number, os.strerror(error_number))
|
@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- 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
|
||||
|
||||
if __name__ == '__main__':
|
||||
import inspect
|
||||
file_path = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe())))
|
||||
sys.path.insert(0, os.path.join(file_path, '../'))
|
||||
|
||||
from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns
|
||||
|
||||
|
||||
def main():
|
||||
shell.check_python3()
|
||||
|
||||
# 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)
|
||||
|
||||
if not config.get('dns_ipv6', False):
|
||||
asyncdns.IPV6_CONNECTION_SUPPORT = False
|
||||
|
||||
daemon.daemon_exec(config)
|
||||
logging.info("local start with protocol[%s] password [%s] method [%s] obfs [%s] obfs_param [%s]" %
|
||||
(config['protocol'], config['password'], config['method'], config['obfs'], config['obfs_param']))
|
||||
|
||||
try:
|
||||
logging.info("starting local at %s:%d" %
|
||||
(config['local_address'], config['local_port']))
|
||||
|
||||
dns_resolver = asyncdns.DNSResolver()
|
||||
tcp_server = tcprelay.TCPRelay(config, dns_resolver, True)
|
||||
udp_server = udprelay.UDPRelay(config, dns_resolver, True)
|
||||
loop = eventloop.EventLoop()
|
||||
dns_resolver.add_to_loop(loop)
|
||||
tcp_server.add_to_loop(loop)
|
||||
udp_server.add_to_loop(loop)
|
||||
|
||||
def handler(signum, _):
|
||||
logging.warn('received SIGQUIT, doing graceful shutting down..')
|
||||
tcp_server.close(next_tick=True)
|
||||
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()
|
||||
except Exception as e:
|
||||
shell.print_exception(e)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd `dirname $0`
|
||||
eval $(ps -ef | grep "[0-9] python3 server\\.py a" | awk '{print "kill "$2}')
|
||||
ulimit -n 512000
|
||||
nohup python3 server.py a >> ssserver.log 2>&1 &
|
@ -0,0 +1,179 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 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 collections
|
||||
import logging
|
||||
import time
|
||||
|
||||
if __name__ == '__main__':
|
||||
import os, sys, inspect
|
||||
file_path = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe())))
|
||||
sys.path.insert(0, os.path.join(file_path, '../'))
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except:
|
||||
from shadowsocks.ordereddict import OrderedDict
|
||||
|
||||
# this LRUCache is optimized for concurrency, not QPS
|
||||
# n: concurrency, keys stored in the cache
|
||||
# m: visits not timed out, proportional to QPS * timeout
|
||||
# get & set is O(1), not O(n). thus we can support very large n
|
||||
# sweep is O((n - m)) or O(1024) at most,
|
||||
# no metter how large the cache or timeout value is
|
||||
|
||||
SWEEP_MAX_ITEMS = 1024
|
||||
|
||||
class LRUCache(collections.MutableMapping):
|
||||
"""This class is not thread safe"""
|
||||
|
||||
def __init__(self, timeout=60, close_callback=None, *args, **kwargs):
|
||||
self.timeout = timeout
|
||||
self.close_callback = close_callback
|
||||
self._store = {}
|
||||
self._keys_to_last_time = OrderedDict()
|
||||
self.update(dict(*args, **kwargs)) # use the free update to set keys
|
||||
|
||||
def __getitem__(self, key):
|
||||
# O(1)
|
||||
t = time.time()
|
||||
last_t = self._keys_to_last_time[key]
|
||||
del self._keys_to_last_time[key]
|
||||
self._keys_to_last_time[key] = t
|
||||
return self._store[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# O(1)
|
||||
t = time.time()
|
||||
if key in self._keys_to_last_time:
|
||||
del self._keys_to_last_time[key]
|
||||
self._keys_to_last_time[key] = t
|
||||
self._store[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
# O(1)
|
||||
last_t = self._keys_to_last_time[key]
|
||||
del self._store[key]
|
||||
del self._keys_to_last_time[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._store
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._store)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._store)
|
||||
|
||||
def first(self):
|
||||
if len(self._keys_to_last_time) > 0:
|
||||
for key in self._keys_to_last_time:
|
||||
return key
|
||||
|
||||
def sweep(self, sweep_item_cnt = SWEEP_MAX_ITEMS):
|
||||
# O(n - m)
|
||||
now = time.time()
|
||||
c = 0
|
||||
while c < sweep_item_cnt:
|
||||
if len(self._keys_to_last_time) == 0:
|
||||
break
|
||||
for key in self._keys_to_last_time:
|
||||
break
|
||||
last_t = self._keys_to_last_time[key]
|
||||
if now - last_t <= self.timeout:
|
||||
break
|
||||
value = self._store[key]
|
||||
del self._store[key]
|
||||
del self._keys_to_last_time[key]
|
||||
if self.close_callback is not None:
|
||||
self.close_callback(value)
|
||||
c += 1
|
||||
if c:
|
||||
logging.debug('%d keys swept' % c)
|
||||
return c < SWEEP_MAX_ITEMS
|
||||
|
||||
def clear(self, keep):
|
||||
now = time.time()
|
||||
c = 0
|
||||
while len(self._keys_to_last_time) > keep:
|
||||
if len(self._keys_to_last_time) == 0:
|
||||
break
|
||||
for key in self._keys_to_last_time:
|
||||
break
|
||||
last_t = self._keys_to_last_time[key]
|
||||
value = self._store[key]
|
||||
if self.close_callback is not None:
|
||||
self.close_callback(value)
|
||||
del self._store[key]
|
||||
del self._keys_to_last_time[key]
|
||||
c += 1
|
||||
if c:
|
||||
logging.debug('%d keys swept' % c)
|
||||
return c < SWEEP_MAX_ITEMS
|
||||
|
||||
def test():
|
||||
c = LRUCache(timeout=0.3)
|
||||
|
||||
c['a'] = 1
|
||||
assert c['a'] == 1
|
||||
c['a'] = 1
|
||||
|
||||
time.sleep(0.5)
|
||||
c.sweep()
|
||||
assert 'a' not in c
|
||||
|
||||
c['a'] = 2
|
||||
c['b'] = 3
|
||||
time.sleep(0.2)
|
||||
c.sweep()
|
||||
assert c['a'] == 2
|
||||
assert c['b'] == 3
|
||||
|
||||
time.sleep(0.2)
|
||||
c.sweep()
|
||||
c['b']
|
||||
time.sleep(0.2)
|
||||
c.sweep()
|
||||
assert 'a' not in c
|
||||
assert c['b'] == 3
|
||||
|
||||
time.sleep(0.5)
|
||||
c.sweep()
|
||||
assert 'a' not in c
|
||||
assert 'b' not in c
|
||||
|
||||
global close_cb_called
|
||||
close_cb_called = False
|
||||
|
||||
def close_cb(t):
|
||||
global close_cb_called
|
||||
assert not close_cb_called
|
||||
close_cb_called = True
|
||||
|
||||
c = LRUCache(timeout=0.1, close_callback=close_cb)
|
||||
c['s'] = 1
|
||||
c['s']
|
||||
time.sleep(0.1)
|
||||
c['s']
|
||||
time.sleep(0.3)
|
||||
c.sweep()
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
@ -0,0 +1,291 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 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 errno
|
||||
import traceback
|
||||
import socket
|
||||
import logging
|
||||
import json
|
||||
import collections
|
||||
|
||||
from shadowsocks import common, eventloop, tcprelay, udprelay, asyncdns, shell
|
||||
|
||||
|
||||
BUF_SIZE = 1506
|
||||
STAT_SEND_LIMIT = 50
|
||||
|
||||
|
||||
class Manager(object):
|
||||
|
||||
def __init__(self, config):
|
||||
self._config = config
|
||||
self._relays = {} # (tcprelay, udprelay)
|
||||
self._loop = eventloop.EventLoop()
|
||||
self._dns_resolver = asyncdns.DNSResolver()
|
||||
self._dns_resolver.add_to_loop(self._loop)
|
||||
|
||||
self._statistics = collections.defaultdict(int)
|
||||
self._control_client_addr = None
|
||||
try:
|
||||
manager_address = common.to_str(config['manager_address'])
|
||||
if ':' in manager_address:
|
||||
addr = manager_address.rsplit(':', 1)
|
||||
addr = addr[0], int(addr[1])
|
||||
addrs = socket.getaddrinfo(addr[0], addr[1])
|
||||
if addrs:
|
||||
family = addrs[0][0]
|
||||
else:
|
||||
logging.error('invalid address: %s', manager_address)
|
||||
exit(1)
|
||||
else:
|
||||
addr = manager_address
|
||||
family = socket.AF_UNIX
|
||||
self._control_socket = socket.socket(family,
|
||||
socket.SOCK_DGRAM)
|
||||
self._control_socket.bind(addr)
|
||||
self._control_socket.setblocking(False)
|
||||
except (OSError, IOError) as e:
|
||||
logging.error(e)
|
||||
logging.error('can not bind to manager address')
|
||||
exit(1)
|
||||
self._loop.add(self._control_socket,
|
||||
eventloop.POLL_IN, self)
|
||||
self._loop.add_periodic(self.handle_periodic)
|
||||
|
||||
port_password = config['port_password']
|
||||
del config['port_password']
|
||||
for port, password in port_password.items():
|
||||
a_config = config.copy()
|
||||
a_config['server_port'] = int(port)
|
||||
a_config['password'] = password
|
||||
self.add_port(a_config)
|
||||
|
||||
def add_port(self, config):
|
||||
port = int(config['server_port'])
|
||||
servers = self._relays.get(port, None)
|
||||
if servers:
|
||||
logging.error("server already exists at %s:%d" % (config['server'],
|
||||
port))
|
||||
return
|
||||
logging.info("adding server at %s:%d" % (config['server'], port))
|
||||
t = tcprelay.TCPRelay(config, self._dns_resolver, False,
|
||||
stat_callback=self.stat_callback)
|
||||
u = udprelay.UDPRelay(config, self._dns_resolver, False,
|
||||
stat_callback=self.stat_callback)
|
||||
t.add_to_loop(self._loop)
|
||||
u.add_to_loop(self._loop)
|
||||
self._relays[port] = (t, u)
|
||||
|
||||
def remove_port(self, config):
|
||||
port = int(config['server_port'])
|
||||
servers = self._relays.get(port, None)
|
||||
if servers:
|
||||
logging.info("removing server at %s:%d" % (config['server'], port))
|
||||
t, u = servers
|
||||
t.close(next_tick=False)
|
||||
u.close(next_tick=False)
|
||||
del self._relays[port]
|
||||
else:
|
||||
logging.error("server not exist at %s:%d" % (config['server'],
|
||||
port))
|
||||
|
||||
def handle_event(self, sock, fd, event):
|
||||
if sock == self._control_socket and event == eventloop.POLL_IN:
|
||||
data, self._control_client_addr = sock.recvfrom(BUF_SIZE)
|
||||
parsed = self._parse_command(data)
|
||||
if parsed:
|
||||
command, config = parsed
|
||||
a_config = self._config.copy()
|
||||
if config:
|
||||
# let the command override the configuration file
|
||||
a_config.update(config)
|
||||
if 'server_port' not in a_config:
|
||||
logging.error('can not find server_port in config')
|
||||
else:
|
||||
if command == 'add':
|
||||
self.add_port(a_config)
|
||||
self._send_control_data(b'ok')
|
||||
elif command == 'remove':
|
||||
self.remove_port(a_config)
|
||||
self._send_control_data(b'ok')
|
||||
elif command == 'ping':
|
||||
self._send_control_data(b'pong')
|
||||
else:
|
||||
logging.error('unknown command %s', command)
|
||||
|
||||
def _parse_command(self, data):
|
||||
# commands:
|
||||
# add: {"server_port": 8000, "password": "foobar"}
|
||||
# remove: {"server_port": 8000"}
|
||||
data = common.to_str(data)
|
||||
parts = data.split(':', 1)
|
||||
if len(parts) < 2:
|
||||
return data, None
|
||||
command, config_json = parts
|
||||
try:
|
||||
config = shell.parse_json_in_str(config_json)
|
||||
return command, config
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
return None
|
||||
|
||||
def stat_callback(self, port, data_len):
|
||||
self._statistics[port] += data_len
|
||||
|
||||
def handle_periodic(self):
|
||||
r = {}
|
||||
i = 0
|
||||
|
||||
def send_data(data_dict):
|
||||
if data_dict:
|
||||
# use compact JSON format (without space)
|
||||
data = common.to_bytes(json.dumps(data_dict,
|
||||
separators=(',', ':')))
|
||||
self._send_control_data(b'stat: ' + data)
|
||||
|
||||
for k, v in self._statistics.items():
|
||||
r[k] = v
|
||||
i += 1
|
||||
# split the data into segments that fit in UDP packets
|
||||
if i >= STAT_SEND_LIMIT:
|
||||
send_data(r)
|
||||
r.clear()
|
||||
i = 0
|
||||
if len(r) > 0 :
|
||||
send_data(r)
|
||||
self._statistics.clear()
|
||||
|
||||
def _send_control_data(self, data):
|
||||
if self._control_client_addr:
|
||||
try:
|
||||
self._control_socket.sendto(data, self._control_client_addr)
|
||||
except (socket.error, OSError, IOError) as e:
|
||||
error_no = eventloop.errno_from_exception(e)
|
||||
if error_no in (errno.EAGAIN, errno.EINPROGRESS,
|
||||
errno.EWOULDBLOCK):
|
||||
return
|
||||
else:
|
||||
shell.print_exception(e)
|
||||
if self._config['verbose']:
|
||||
traceback.print_exc()
|
||||
|
||||
def run(self):
|
||||
self._loop.run()
|
||||
|
||||
|
||||
def run(config):
|
||||
Manager(config).run()
|
||||
|
||||
|
||||
def test():
|
||||
import time
|
||||
import threading
|
||||
import struct
|
||||
from shadowsocks import encrypt
|
||||
|
||||
logging.basicConfig(level=5,
|
||||
format='%(asctime)s %(levelname)-8s %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S')
|
||||
enc = []
|
||||
eventloop.TIMEOUT_PRECISION = 1
|
||||
|
||||
def run_server():
|
||||
config = shell.get_config(True)
|
||||
config = config.copy()
|
||||
a_config = {
|
||||
'server': '127.0.0.1',
|
||||
'local_port': 1081,
|
||||
'port_password': {
|
||||
'8381': 'foobar1',
|
||||
'8382': 'foobar2'
|
||||
},
|
||||
'method': 'aes-256-cfb',
|
||||
'manager_address': '127.0.0.1:6001',
|
||||
'timeout': 60,
|
||||
'fast_open': False,
|
||||
'verbose': 2
|
||||
}
|
||||
config.update(a_config)
|
||||
manager = Manager(config)
|
||||
enc.append(manager)
|
||||
manager.run()
|
||||
|
||||
t = threading.Thread(target=run_server)
|
||||
t.start()
|
||||
time.sleep(1)
|
||||
manager = enc[0]
|
||||
cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
cli.connect(('127.0.0.1', 6001))
|
||||
|
||||
# test add and remove
|
||||
time.sleep(1)
|
||||
cli.send(b'add: {"server_port":7001, "password":"asdfadsfasdf"}')
|
||||
time.sleep(1)
|
||||
assert 7001 in manager._relays
|
||||
data, addr = cli.recvfrom(1506)
|
||||
assert b'ok' in data
|
||||
|
||||
cli.send(b'remove: {"server_port":8381}')
|
||||
time.sleep(1)
|
||||
assert 8381 not in manager._relays
|
||||
data, addr = cli.recvfrom(1506)
|
||||
assert b'ok' in data
|
||||
logging.info('add and remove test passed')
|
||||
|
||||
# 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,
|
||||
header + b'GET /\r\n\r\n')
|
||||
tcp_cli = socket.socket()
|
||||
tcp_cli.connect(('127.0.0.1', 7001))
|
||||
tcp_cli.send(data)
|
||||
tcp_cli.recv(4096)
|
||||
tcp_cli.close()
|
||||
|
||||
data, addr = cli.recvfrom(1506)
|
||||
data = common.to_str(data)
|
||||
assert data.startswith('stat: ')
|
||||
data = data.split('stat:')[1]
|
||||
stats = shell.parse_json_in_str(data)
|
||||
assert '7001' in stats
|
||||
logging.info('TCP statistics test passed')
|
||||
|
||||
# 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,
|
||||
header + b'test')
|
||||
udp_cli = socket.socket(type=socket.SOCK_DGRAM)
|
||||
udp_cli.sendto(data, ('127.0.0.1', 8382))
|
||||
tcp_cli.close()
|
||||
|
||||
data, addr = cli.recvfrom(1506)
|
||||
data = common.to_str(data)
|
||||
assert data.startswith('stat: ')
|
||||
data = data.split('stat:')[1]
|
||||
stats = json.loads(data)
|
||||
assert '8382' in stats
|
||||
logging.info('UDP statistics test passed')
|
||||
|
||||
manager._loop.stop()
|
||||
t.join()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2015-2015 breakwa11
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from shadowsocks import common
|
||||
from shadowsocks.obfsplugin import plain, http_simple, obfs_tls, verify, auth, auth_chain
|
||||
|
||||
|
||||
method_supported = {}
|
||||
method_supported.update(plain.obfs_map)
|
||||
method_supported.update(http_simple.obfs_map)
|
||||
method_supported.update(obfs_tls.obfs_map)
|
||||
method_supported.update(verify.obfs_map)
|
||||
method_supported.update(auth.obfs_map)
|
||||
method_supported.update(auth_chain.obfs_map)
|
||||
|
||||
def mu_protocol():
|
||||
return ["auth_aes128_md5", "auth_aes128_sha1", "auth_chain_a"]
|
||||
|
||||
class server_info(object):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
class obfs(object):
|
||||
def __init__(self, method):
|
||||
method = common.to_str(method)
|
||||
self.method = method
|
||||
self._method_info = self.get_method_info(method)
|
||||
if self._method_info:
|
||||
self.obfs = self.get_obfs(method)
|
||||
else:
|
||||
raise Exception('obfs plugin [%s] not supported' % method)
|
||||
|
||||
def init_data(self):
|
||||
return self.obfs.init_data()
|
||||
|
||||
def set_server_info(self, server_info):
|
||||
return self.obfs.set_server_info(server_info)
|
||||
|
||||
def get_server_info(self):
|
||||
return self.obfs.get_server_info()
|
||||
|
||||
def get_method_info(self, method):
|
||||
method = method.lower()
|
||||
m = method_supported.get(method)
|
||||
return m
|
||||
|
||||
def get_obfs(self, method):
|
||||
m = self._method_info
|
||||
return m[0](method)
|
||||
|
||||
def get_overhead(self, direction):
|
||||
return self.obfs.get_overhead(direction)
|
||||
|
||||
def client_pre_encrypt(self, buf):
|
||||
return self.obfs.client_pre_encrypt(buf)
|
||||
|
||||
def client_encode(self, buf):
|
||||
return self.obfs.client_encode(buf)
|
||||
|
||||
def client_decode(self, buf):
|
||||
return self.obfs.client_decode(buf)
|
||||
|
||||
def client_post_decrypt(self, buf):
|
||||
return self.obfs.client_post_decrypt(buf)
|
||||
|
||||
def server_pre_encrypt(self, buf):
|
||||
return self.obfs.server_pre_encrypt(buf)
|
||||
|
||||
def server_encode(self, buf):
|
||||
return self.obfs.server_encode(buf)
|
||||
|
||||
def server_decode(self, buf):
|
||||
return self.obfs.server_decode(buf)
|
||||
|
||||
def server_post_decrypt(self, buf):
|
||||
return self.obfs.server_post_decrypt(buf)
|
||||
|
||||
def client_udp_pre_encrypt(self, buf):
|
||||
return self.obfs.client_udp_pre_encrypt(buf)
|
||||
|
||||
def client_udp_post_decrypt(self, buf):
|
||||
return self.obfs.client_udp_post_decrypt(buf)
|
||||
|
||||
def server_udp_pre_encrypt(self, buf, uid):
|
||||
return self.obfs.server_udp_pre_encrypt(buf, uid)
|
||||
|
||||
def server_udp_post_decrypt(self, buf):
|
||||
return self.obfs.server_udp_post_decrypt(buf)
|
||||
|
||||
def dispose(self):
|
||||
self.obfs.dispose()
|
||||
del self.obfs
|
||||
|
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 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
|
@ -0,0 +1,787 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2015-2015 breakwa11
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
import binascii
|
||||
import base64
|
||||
import time
|
||||
import datetime
|
||||
import random
|
||||
import math
|
||||
import struct
|
||||
import zlib
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
import shadowsocks
|
||||
from shadowsocks import common, lru_cache, encrypt
|
||||
from shadowsocks.obfsplugin import plain
|
||||
from shadowsocks.common import to_bytes, to_str, ord, chr
|
||||
|
||||
def create_auth_sha1_v4(method):
|
||||
return auth_sha1_v4(method)
|
||||
|
||||
def create_auth_aes128_md5(method):
|
||||
return auth_aes128_sha1(method, hashlib.md5)
|
||||
|
||||
def create_auth_aes128_sha1(method):
|
||||
return auth_aes128_sha1(method, hashlib.sha1)
|
||||
|
||||
obfs_map = {
|
||||
'auth_sha1_v4': (create_auth_sha1_v4,),
|
||||
'auth_sha1_v4_compatible': (create_auth_sha1_v4,),
|
||||
'auth_aes128_md5': (create_auth_aes128_md5,),
|
||||
'auth_aes128_sha1': (create_auth_aes128_sha1,),
|
||||
}
|
||||
|
||||
def match_begin(str1, str2):
|
||||
if len(str1) >= len(str2):
|
||||
if str1[:len(str2)] == str2:
|
||||
return True
|
||||
return False
|
||||
|
||||
class auth_base(plain.plain):
|
||||
def __init__(self, method):
|
||||
super(auth_base, self).__init__(method)
|
||||
self.method = method
|
||||
self.no_compatible_method = ''
|
||||
self.overhead = 7
|
||||
|
||||
def init_data(self):
|
||||
return ''
|
||||
|
||||
def get_overhead(self, direction): # direction: true for c->s false for s->c
|
||||
return self.overhead
|
||||
|
||||
def set_server_info(self, server_info):
|
||||
self.server_info = server_info
|
||||
|
||||
def client_encode(self, buf):
|
||||
return buf
|
||||
|
||||
def client_decode(self, buf):
|
||||
return (buf, False)
|
||||
|
||||
def server_encode(self, buf):
|
||||
return buf
|
||||
|
||||
def server_decode(self, buf):
|
||||
return (buf, True, False)
|
||||
|
||||
def not_match_return(self, buf):
|
||||
self.raw_trans = True
|
||||
self.overhead = 0
|
||||
if self.method == self.no_compatible_method:
|
||||
return (b'E'*2048, False)
|
||||
return (buf, False)
|
||||
|
||||
class client_queue(object):
|
||||
def __init__(self, begin_id):
|
||||
self.front = begin_id - 64
|
||||
self.back = begin_id + 1
|
||||
self.alloc = {}
|
||||
self.enable = True
|
||||
self.last_update = time.time()
|
||||
|
||||
def update(self):
|
||||
self.last_update = time.time()
|
||||
|
||||
def is_active(self):
|
||||
return time.time() - self.last_update < 60 * 3
|
||||
|
||||
def re_enable(self, connection_id):
|
||||
self.enable = True
|
||||
self.front = connection_id - 64
|
||||
self.back = connection_id + 1
|
||||
self.alloc = {}
|
||||
|
||||
def insert(self, connection_id):
|
||||
if not self.enable:
|
||||
logging.warn('obfs auth: not enable')
|
||||
return False
|
||||
if not self.is_active():
|
||||
self.re_enable(connection_id)
|
||||
self.update()
|
||||
if connection_id < self.front:
|
||||
logging.warn('obfs auth: deprecated id, someone replay attack')
|
||||
return False
|
||||
if connection_id > self.front + 0x4000:
|
||||
logging.warn('obfs auth: wrong id')
|
||||
return False
|
||||
if connection_id in self.alloc:
|
||||
logging.warn('obfs auth: duplicate id, someone replay attack')
|
||||
return False
|
||||
if self.back <= connection_id:
|
||||
self.back = connection_id + 1
|
||||
self.alloc[connection_id] = 1
|
||||
while (self.front in self.alloc) or self.front + 0x1000 < self.back:
|
||||
if self.front in self.alloc:
|
||||
del self.alloc[self.front]
|
||||
self.front += 1
|
||||
return True
|
||||
|
||||
class obfs_auth_v2_data(object):
|
||||
def __init__(self):
|
||||
self.client_id = lru_cache.LRUCache()
|
||||
self.local_client_id = b''
|
||||
self.connection_id = 0
|
||||
self.set_max_client(64) # max active client count
|
||||
|
||||
def update(self, client_id, connection_id):
|
||||
if client_id in self.client_id:
|
||||
self.client_id[client_id].update()
|
||||
|
||||
def set_max_client(self, max_client):
|
||||
self.max_client = max_client
|
||||
self.max_buffer = max(self.max_client * 2, 1024)
|
||||
|
||||
def insert(self, client_id, connection_id):
|
||||
if self.client_id.get(client_id, None) is None or not self.client_id[client_id].enable:
|
||||
if self.client_id.first() is None or len(self.client_id) < self.max_client:
|
||||
if client_id not in self.client_id:
|
||||
#TODO: check
|
||||
self.client_id[client_id] = client_queue(connection_id)
|
||||
else:
|
||||
self.client_id[client_id].re_enable(connection_id)
|
||||
return self.client_id[client_id].insert(connection_id)
|
||||
|
||||
if not self.client_id[self.client_id.first()].is_active():
|
||||
del self.client_id[self.client_id.first()]
|
||||
if client_id not in self.client_id:
|
||||
#TODO: check
|
||||
self.client_id[client_id] = client_queue(connection_id)
|
||||
else:
|
||||
self.client_id[client_id].re_enable(connection_id)
|
||||
return self.client_id[client_id].insert(connection_id)
|
||||
|
||||
logging.warn('auth_sha1_v2: no inactive client')
|
||||
return False
|
||||
else:
|
||||
return self.client_id[client_id].insert(connection_id)
|
||||
|
||||
class auth_sha1_v4(auth_base):
|
||||
def __init__(self, method):
|
||||
super(auth_sha1_v4, self).__init__(method)
|
||||
self.recv_buf = b''
|
||||
self.unit_len = 8100
|
||||
self.decrypt_packet_num = 0
|
||||
self.raw_trans = False
|
||||
self.has_sent_header = False
|
||||
self.has_recv_header = False
|
||||
self.client_id = 0
|
||||
self.connection_id = 0
|
||||
self.max_time_dif = 60 * 60 * 24 # time dif (second) setting
|
||||
self.salt = b"auth_sha1_v4"
|
||||
self.no_compatible_method = 'auth_sha1_v4'
|
||||
|
||||
def init_data(self):
|
||||
return obfs_auth_v2_data()
|
||||
|
||||
def set_server_info(self, server_info):
|
||||
self.server_info = server_info
|
||||
try:
|
||||
max_client = int(server_info.protocol_param)
|
||||
except:
|
||||
max_client = 64
|
||||
self.server_info.data.set_max_client(max_client)
|
||||
|
||||
def rnd_data(self, buf_size):
|
||||
if buf_size > 1200:
|
||||
return b'\x01'
|
||||
|
||||
if buf_size > 400:
|
||||
rnd_data = os.urandom(common.ord(os.urandom(1)[0]) % 256)
|
||||
else:
|
||||
rnd_data = os.urandom(struct.unpack('>H', os.urandom(2))[0] % 512)
|
||||
|
||||
if len(rnd_data) < 128:
|
||||
return common.chr(len(rnd_data) + 1) + rnd_data
|
||||
else:
|
||||
return common.chr(255) + struct.pack('>H', len(rnd_data) + 3) + rnd_data
|
||||
|
||||
def pack_data(self, buf):
|
||||
data = self.rnd_data(len(buf)) + buf
|
||||
data_len = len(data) + 8
|
||||
crc = binascii.crc32(struct.pack('>H', data_len)) & 0xFFFF
|
||||
data = struct.pack('<H', crc) + data
|
||||
data = struct.pack('>H', data_len) + data
|
||||
adler32 = zlib.adler32(data) & 0xFFFFFFFF
|
||||
data += struct.pack('<I', adler32)
|
||||
return data
|
||||
|
||||
def pack_auth_data(self, buf):
|
||||
if len(buf) == 0:
|
||||
return b''
|
||||
data = self.rnd_data(len(buf)) + buf
|
||||
data_len = len(data) + 16
|
||||
crc = binascii.crc32(struct.pack('>H', data_len) + self.salt + self.server_info.key) & 0xFFFFFFFF
|
||||
data = struct.pack('<I', crc) + data
|
||||
data = struct.pack('>H', data_len) + data
|
||||
data += hmac.new(self.server_info.iv + self.server_info.key, data, hashlib.sha1).digest()[:10]
|
||||
return data
|
||||
|
||||
def auth_data(self):
|
||||
utc_time = int(time.time()) & 0xFFFFFFFF
|
||||
if self.server_info.data.connection_id > 0xFF000000:
|
||||
self.server_info.data.local_client_id = b''
|
||||
if not self.server_info.data.local_client_id:
|
||||
self.server_info.data.local_client_id = os.urandom(4)
|
||||
logging.debug("local_client_id %s" % (binascii.hexlify(self.server_info.data.local_client_id),))
|
||||
self.server_info.data.connection_id = struct.unpack('<I', os.urandom(4))[0] & 0xFFFFFF
|
||||
self.server_info.data.connection_id += 1
|
||||
return b''.join([struct.pack('<I', utc_time),
|
||||
self.server_info.data.local_client_id,
|
||||
struct.pack('<I', self.server_info.data.connection_id)])
|
||||
|
||||
def client_pre_encrypt(self, buf):
|
||||
ret = b''
|
||||
if not self.has_sent_header:
|
||||
head_size = self.get_head_size(buf, 30)
|
||||
datalen = min(len(buf), random.randint(0, 31) + head_size)
|
||||
ret += self.pack_auth_data(self.auth_data() + buf[:datalen])
|
||||
buf = buf[datalen:]
|
||||
self.has_sent_header = True
|
||||
while len(buf) > self.unit_len:
|
||||
ret += self.pack_data(buf[:self.unit_len])
|
||||
buf = buf[self.unit_len:]
|
||||
ret += self.pack_data(buf)
|
||||
return ret
|
||||
|
||||
def client_post_decrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return buf
|
||||
self.recv_buf += buf
|
||||
out_buf = b''
|
||||
while len(self.recv_buf) > 4:
|
||||
crc = struct.pack('<H', binascii.crc32(self.recv_buf[:2]) & 0xFFFF)
|
||||
if crc != self.recv_buf[2:4]:
|
||||
raise Exception('client_post_decrypt data uncorrect crc')
|
||||
length = struct.unpack('>H', self.recv_buf[:2])[0]
|
||||
if length >= 8192 or length < 7:
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
raise Exception('client_post_decrypt data error')
|
||||
if length > len(self.recv_buf):
|
||||
break
|
||||
|
||||
if struct.pack('<I', zlib.adler32(self.recv_buf[:length - 4]) & 0xFFFFFFFF) != self.recv_buf[length - 4:length]:
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
raise Exception('client_post_decrypt data uncorrect checksum')
|
||||
|
||||
pos = common.ord(self.recv_buf[4])
|
||||
if pos < 255:
|
||||
pos += 4
|
||||
else:
|
||||
pos = struct.unpack('>H', self.recv_buf[5:7])[0] + 4
|
||||
out_buf += self.recv_buf[pos:length - 4]
|
||||
self.recv_buf = self.recv_buf[length:]
|
||||
|
||||
if out_buf:
|
||||
self.decrypt_packet_num += 1
|
||||
return out_buf
|
||||
|
||||
def server_pre_encrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return buf
|
||||
ret = b''
|
||||
while len(buf) > self.unit_len:
|
||||
ret += self.pack_data(buf[:self.unit_len])
|
||||
buf = buf[self.unit_len:]
|
||||
ret += self.pack_data(buf)
|
||||
return ret
|
||||
|
||||
def server_post_decrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return (buf, False)
|
||||
self.recv_buf += buf
|
||||
out_buf = b''
|
||||
sendback = False
|
||||
|
||||
if not self.has_recv_header:
|
||||
if len(self.recv_buf) <= 6:
|
||||
return (b'', False)
|
||||
crc = struct.pack('<I', binascii.crc32(self.recv_buf[:2] + self.salt + self.server_info.key) & 0xFFFFFFFF)
|
||||
if crc != self.recv_buf[2:6]:
|
||||
return self.not_match_return(self.recv_buf)
|
||||
length = struct.unpack('>H', self.recv_buf[:2])[0]
|
||||
if length > len(self.recv_buf):
|
||||
return (b'', False)
|
||||
sha1data = hmac.new(self.server_info.recv_iv + self.server_info.key, self.recv_buf[:length - 10], hashlib.sha1).digest()[:10]
|
||||
if sha1data != self.recv_buf[length - 10:length]:
|
||||
logging.error('auth_sha1_v4 data uncorrect auth HMAC-SHA1')
|
||||
return self.not_match_return(self.recv_buf)
|
||||
pos = common.ord(self.recv_buf[6])
|
||||
if pos < 255:
|
||||
pos += 6
|
||||
else:
|
||||
pos = struct.unpack('>H', self.recv_buf[7:9])[0] + 6
|
||||
out_buf = self.recv_buf[pos:length - 10]
|
||||
if len(out_buf) < 12:
|
||||
logging.info('auth_sha1_v4: too short, data %s' % (binascii.hexlify(self.recv_buf),))
|
||||
return self.not_match_return(self.recv_buf)
|
||||
utc_time = struct.unpack('<I', out_buf[:4])[0]
|
||||
client_id = struct.unpack('<I', out_buf[4:8])[0]
|
||||
connection_id = struct.unpack('<I', out_buf[8:12])[0]
|
||||
time_dif = common.int32(utc_time - (int(time.time()) & 0xffffffff))
|
||||
if time_dif < -self.max_time_dif or time_dif > self.max_time_dif:
|
||||
logging.info('auth_sha1_v4: wrong timestamp, time_dif %d, data %s' % (time_dif, binascii.hexlify(out_buf),))
|
||||
return self.not_match_return(self.recv_buf)
|
||||
elif self.server_info.data.insert(client_id, connection_id):
|
||||
self.has_recv_header = True
|
||||
out_buf = out_buf[12:]
|
||||
self.client_id = client_id
|
||||
self.connection_id = connection_id
|
||||
else:
|
||||
logging.info('auth_sha1_v4: auth fail, data %s' % (binascii.hexlify(out_buf),))
|
||||
return self.not_match_return(self.recv_buf)
|
||||
self.recv_buf = self.recv_buf[length:]
|
||||
self.has_recv_header = True
|
||||
sendback = True
|
||||
|
||||
while len(self.recv_buf) > 4:
|
||||
crc = struct.pack('<H', binascii.crc32(self.recv_buf[:2]) & 0xFFFF)
|
||||
if crc != self.recv_buf[2:4]:
|
||||
self.raw_trans = True
|
||||
logging.info('auth_sha1_v4: wrong crc')
|
||||
if self.decrypt_packet_num == 0:
|
||||
logging.info('auth_sha1_v4: wrong crc')
|
||||
return (b'E'*2048, False)
|
||||
else:
|
||||
raise Exception('server_post_decrype data error')
|
||||
length = struct.unpack('>H', self.recv_buf[:2])[0]
|
||||
if length >= 8192 or length < 7:
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
if self.decrypt_packet_num == 0:
|
||||
logging.info('auth_sha1_v4: over size')
|
||||
return (b'E'*2048, False)
|
||||
else:
|
||||
raise Exception('server_post_decrype data error')
|
||||
if length > len(self.recv_buf):
|
||||
break
|
||||
|
||||
if struct.pack('<I', zlib.adler32(self.recv_buf[:length - 4]) & 0xFFFFFFFF) != self.recv_buf[length - 4:length]:
|
||||
logging.info('auth_sha1_v4: checksum error, data %s' % (binascii.hexlify(self.recv_buf[:length]),))
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
if self.decrypt_packet_num == 0:
|
||||
return (b'E'*2048, False)
|
||||
else:
|
||||
raise Exception('server_post_decrype data uncorrect checksum')
|
||||
|
||||
pos = common.ord(self.recv_buf[4])
|
||||
if pos < 255:
|
||||
pos += 4
|
||||
else:
|
||||
pos = struct.unpack('>H', self.recv_buf[5:7])[0] + 4
|
||||
out_buf += self.recv_buf[pos:length - 4]
|
||||
self.recv_buf = self.recv_buf[length:]
|
||||
if pos == length - 4:
|
||||
sendback = True
|
||||
|
||||
if out_buf:
|
||||
self.server_info.data.update(self.client_id, self.connection_id)
|
||||
self.decrypt_packet_num += 1
|
||||
return (out_buf, sendback)
|
||||
|
||||
class obfs_auth_mu_data(object):
|
||||
def __init__(self):
|
||||
self.user_id = {}
|
||||
self.local_client_id = b''
|
||||
self.connection_id = 0
|
||||
self.set_max_client(64) # max active client count
|
||||
|
||||
def update(self, user_id, client_id, connection_id):
|
||||
if user_id not in self.user_id:
|
||||
self.user_id[user_id] = lru_cache.LRUCache()
|
||||
local_client_id = self.user_id[user_id]
|
||||
|
||||
if client_id in local_client_id:
|
||||
local_client_id[client_id].update()
|
||||
|
||||
def set_max_client(self, max_client):
|
||||
self.max_client = max_client
|
||||
self.max_buffer = max(self.max_client * 2, 1024)
|
||||
|
||||
def insert(self, user_id, client_id, connection_id):
|
||||
if user_id not in self.user_id:
|
||||
self.user_id[user_id] = lru_cache.LRUCache()
|
||||
local_client_id = self.user_id[user_id]
|
||||
|
||||
if local_client_id.get(client_id, None) is None or not local_client_id[client_id].enable:
|
||||
if local_client_id.first() is None or len(local_client_id) < self.max_client:
|
||||
if client_id not in local_client_id:
|
||||
#TODO: check
|
||||
local_client_id[client_id] = client_queue(connection_id)
|
||||
else:
|
||||
local_client_id[client_id].re_enable(connection_id)
|
||||
return local_client_id[client_id].insert(connection_id)
|
||||
|
||||
if not local_client_id[local_client_id.first()].is_active():
|
||||
del local_client_id[local_client_id.first()]
|
||||
if client_id not in local_client_id:
|
||||
#TODO: check
|
||||
local_client_id[client_id] = client_queue(connection_id)
|
||||
else:
|
||||
local_client_id[client_id].re_enable(connection_id)
|
||||
return local_client_id[client_id].insert(connection_id)
|
||||
|
||||
logging.warn('auth_aes128: no inactive client')
|
||||
return False
|
||||
else:
|
||||
return local_client_id[client_id].insert(connection_id)
|
||||
|
||||
class auth_aes128_sha1(auth_base):
|
||||
def __init__(self, method, hashfunc):
|
||||
super(auth_aes128_sha1, self).__init__(method)
|
||||
self.hashfunc = hashfunc
|
||||
self.recv_buf = b''
|
||||
self.unit_len = 8100
|
||||
self.raw_trans = False
|
||||
self.has_sent_header = False
|
||||
self.has_recv_header = False
|
||||
self.client_id = 0
|
||||
self.connection_id = 0
|
||||
self.max_time_dif = 60 * 60 * 24 # time dif (second) setting
|
||||
self.salt = hashfunc == hashlib.md5 and b"auth_aes128_md5" or b"auth_aes128_sha1"
|
||||
self.no_compatible_method = hashfunc == hashlib.md5 and "auth_aes128_md5" or 'auth_aes128_sha1'
|
||||
self.extra_wait_size = struct.unpack('>H', os.urandom(2))[0] % 1024
|
||||
self.pack_id = 1
|
||||
self.recv_id = 1
|
||||
self.user_id = None
|
||||
self.user_key = None
|
||||
self.last_rnd_len = 0
|
||||
self.overhead = 9
|
||||
|
||||
def init_data(self):
|
||||
return obfs_auth_mu_data()
|
||||
|
||||
def get_overhead(self, direction): # direction: true for c->s false for s->c
|
||||
return self.overhead
|
||||
|
||||
def set_server_info(self, server_info):
|
||||
self.server_info = server_info
|
||||
try:
|
||||
max_client = int(server_info.protocol_param.split('#')[0])
|
||||
except:
|
||||
max_client = 64
|
||||
self.server_info.data.set_max_client(max_client)
|
||||
|
||||
def trapezoid_random_float(self, d):
|
||||
if d == 0:
|
||||
return random.random()
|
||||
s = random.random()
|
||||
a = 1 - d
|
||||
return (math.sqrt(a * a + 4 * d * s) - a) / (2 * d)
|
||||
|
||||
def trapezoid_random_int(self, max_val, d):
|
||||
v = self.trapezoid_random_float(d)
|
||||
return int(v * max_val)
|
||||
|
||||
def rnd_data_len(self, buf_size, full_buf_size):
|
||||
if full_buf_size >= self.server_info.buffer_size:
|
||||
return 0
|
||||
tcp_mss = self.server_info.tcp_mss
|
||||
rev_len = tcp_mss - buf_size - 9
|
||||
if rev_len == 0:
|
||||
return 0
|
||||
if rev_len < 0:
|
||||
if rev_len > -tcp_mss:
|
||||
return self.trapezoid_random_int(rev_len + tcp_mss, -0.3)
|
||||
return common.ord(os.urandom(1)[0]) % 32
|
||||
if buf_size > 900:
|
||||
return struct.unpack('>H', os.urandom(2))[0] % rev_len
|
||||
return self.trapezoid_random_int(rev_len, -0.3)
|
||||
|
||||
def rnd_data(self, buf_size, full_buf_size):
|
||||
data_len = self.rnd_data_len(buf_size, full_buf_size)
|
||||
|
||||
if data_len < 128:
|
||||
return common.chr(data_len + 1) + os.urandom(data_len)
|
||||
|
||||
return common.chr(255) + struct.pack('<H', data_len + 1) + os.urandom(data_len - 2)
|
||||
|
||||
def pack_data(self, buf, full_buf_size):
|
||||
data = self.rnd_data(len(buf), full_buf_size) + buf
|
||||
data_len = len(data) + 8
|
||||
mac_key = self.user_key + struct.pack('<I', self.pack_id)
|
||||
mac = hmac.new(mac_key, struct.pack('<H', data_len), self.hashfunc).digest()[:2]
|
||||
data = struct.pack('<H', data_len) + mac + data
|
||||
data += hmac.new(mac_key, data, self.hashfunc).digest()[:4]
|
||||
self.pack_id = (self.pack_id + 1) & 0xFFFFFFFF
|
||||
return data
|
||||
|
||||
def pack_auth_data(self, auth_data, buf):
|
||||
if len(buf) == 0:
|
||||
return b''
|
||||
if len(buf) > 400:
|
||||
rnd_len = struct.unpack('<H', os.urandom(2))[0] % 512
|
||||
else:
|
||||
rnd_len = struct.unpack('<H', os.urandom(2))[0] % 1024
|
||||
data = auth_data
|
||||
data_len = 7 + 4 + 16 + 4 + len(buf) + rnd_len + 4
|
||||
data = data + struct.pack('<H', data_len) + struct.pack('<H', rnd_len)
|
||||
mac_key = self.server_info.iv + self.server_info.key
|
||||
uid = os.urandom(4)
|
||||
if b':' in to_bytes(self.server_info.protocol_param):
|
||||
try:
|
||||
items = to_bytes(self.server_info.protocol_param).split(b':')
|
||||
self.user_key = self.hashfunc(items[1]).digest()
|
||||
uid = struct.pack('<I', int(items[0]))
|
||||
except:
|
||||
pass
|
||||
if self.user_key is None:
|
||||
self.user_key = self.server_info.key
|
||||
encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(self.user_key)) + self.salt, 'aes-128-cbc', b'\x00' * 16)
|
||||
data = uid + encryptor.encrypt(data)[16:]
|
||||
data += hmac.new(mac_key, data, self.hashfunc).digest()[:4]
|
||||
check_head = os.urandom(1)
|
||||
check_head += hmac.new(mac_key, check_head, self.hashfunc).digest()[:6]
|
||||
data = check_head + data + os.urandom(rnd_len) + buf
|
||||
data += hmac.new(self.user_key, data, self.hashfunc).digest()[:4]
|
||||
return data
|
||||
|
||||
def auth_data(self):
|
||||
utc_time = int(time.time()) & 0xFFFFFFFF
|
||||
if self.server_info.data.connection_id > 0xFF000000:
|
||||
self.server_info.data.local_client_id = b''
|
||||
if not self.server_info.data.local_client_id:
|
||||
self.server_info.data.local_client_id = os.urandom(4)
|
||||
logging.debug("local_client_id %s" % (binascii.hexlify(self.server_info.data.local_client_id),))
|
||||
self.server_info.data.connection_id = struct.unpack('<I', os.urandom(4))[0] & 0xFFFFFF
|
||||
self.server_info.data.connection_id += 1
|
||||
return b''.join([struct.pack('<I', utc_time),
|
||||
self.server_info.data.local_client_id,
|
||||
struct.pack('<I', self.server_info.data.connection_id)])
|
||||
|
||||
def client_pre_encrypt(self, buf):
|
||||
ret = b''
|
||||
ogn_data_len = len(buf)
|
||||
if not self.has_sent_header:
|
||||
head_size = self.get_head_size(buf, 30)
|
||||
datalen = min(len(buf), random.randint(0, 31) + head_size)
|
||||
ret += self.pack_auth_data(self.auth_data(), buf[:datalen])
|
||||
buf = buf[datalen:]
|
||||
self.has_sent_header = True
|
||||
while len(buf) > self.unit_len:
|
||||
ret += self.pack_data(buf[:self.unit_len], ogn_data_len)
|
||||
buf = buf[self.unit_len:]
|
||||
ret += self.pack_data(buf, ogn_data_len)
|
||||
self.last_rnd_len = ogn_data_len
|
||||
return ret
|
||||
|
||||
def client_post_decrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return buf
|
||||
self.recv_buf += buf
|
||||
out_buf = b''
|
||||
while len(self.recv_buf) > 4:
|
||||
mac_key = self.user_key + struct.pack('<I', self.recv_id)
|
||||
mac = hmac.new(mac_key, self.recv_buf[:2], self.hashfunc).digest()[:2]
|
||||
if mac != self.recv_buf[2:4]:
|
||||
raise Exception('client_post_decrypt data uncorrect mac')
|
||||
length = struct.unpack('<H', self.recv_buf[:2])[0]
|
||||
if length >= 8192 or length < 7:
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
raise Exception('client_post_decrypt data error')
|
||||
if length > len(self.recv_buf):
|
||||
break
|
||||
|
||||
if hmac.new(mac_key, self.recv_buf[:length - 4], self.hashfunc).digest()[:4] != self.recv_buf[length - 4:length]:
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
raise Exception('client_post_decrypt data uncorrect checksum')
|
||||
|
||||
self.recv_id = (self.recv_id + 1) & 0xFFFFFFFF
|
||||
pos = common.ord(self.recv_buf[4])
|
||||
if pos < 255:
|
||||
pos += 4
|
||||
else:
|
||||
pos = struct.unpack('<H', self.recv_buf[5:7])[0] + 4
|
||||
out_buf += self.recv_buf[pos:length - 4]
|
||||
self.recv_buf = self.recv_buf[length:]
|
||||
|
||||
return out_buf
|
||||
|
||||
def server_pre_encrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return buf
|
||||
ret = b''
|
||||
ogn_data_len = len(buf)
|
||||
while len(buf) > self.unit_len:
|
||||
ret += self.pack_data(buf[:self.unit_len], ogn_data_len)
|
||||
buf = buf[self.unit_len:]
|
||||
ret += self.pack_data(buf, ogn_data_len)
|
||||
self.last_rnd_len = ogn_data_len
|
||||
return ret
|
||||
|
||||
def server_post_decrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return (buf, False)
|
||||
self.recv_buf += buf
|
||||
out_buf = b''
|
||||
sendback = False
|
||||
|
||||
if not self.has_recv_header:
|
||||
if len(self.recv_buf) >= 7 or len(self.recv_buf) in [2, 3]:
|
||||
recv_len = min(len(self.recv_buf), 7)
|
||||
mac_key = self.server_info.recv_iv + self.server_info.key
|
||||
sha1data = hmac.new(mac_key, self.recv_buf[:1], self.hashfunc).digest()[:recv_len - 1]
|
||||
if sha1data != self.recv_buf[1:recv_len]:
|
||||
return self.not_match_return(self.recv_buf)
|
||||
|
||||
if len(self.recv_buf) < 31:
|
||||
return (b'', False)
|
||||
sha1data = hmac.new(mac_key, self.recv_buf[7:27], self.hashfunc).digest()[:4]
|
||||
if sha1data != self.recv_buf[27:31]:
|
||||
logging.error('%s data uncorrect auth HMAC-SHA1 from %s:%d, data %s' % (self.no_compatible_method, self.server_info.client, self.server_info.client_port, binascii.hexlify(self.recv_buf)))
|
||||
if len(self.recv_buf) < 31 + self.extra_wait_size:
|
||||
return (b'', False)
|
||||
return self.not_match_return(self.recv_buf)
|
||||
|
||||
uid = self.recv_buf[7:11]
|
||||
if uid in self.server_info.users:
|
||||
self.user_id = uid
|
||||
self.user_key = self.hashfunc(self.server_info.users[uid]).digest()
|
||||
self.server_info.update_user_func(uid)
|
||||
else:
|
||||
if not self.server_info.users:
|
||||
self.user_key = self.server_info.key
|
||||
else:
|
||||
self.user_key = self.server_info.recv_iv
|
||||
encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(self.user_key)) + self.salt, 'aes-128-cbc')
|
||||
head = encryptor.decrypt(b'\x00' * 16 + self.recv_buf[11:27] + b'\x00') # need an extra byte or recv empty
|
||||
length = struct.unpack('<H', head[12:14])[0]
|
||||
if len(self.recv_buf) < length:
|
||||
return (b'', False)
|
||||
|
||||
utc_time = struct.unpack('<I', head[:4])[0]
|
||||
client_id = struct.unpack('<I', head[4:8])[0]
|
||||
connection_id = struct.unpack('<I', head[8:12])[0]
|
||||
rnd_len = struct.unpack('<H', head[14:16])[0]
|
||||
if hmac.new(self.user_key, self.recv_buf[:length - 4], self.hashfunc).digest()[:4] != self.recv_buf[length - 4:length]:
|
||||
logging.info('%s: checksum error, data %s' % (self.no_compatible_method, binascii.hexlify(self.recv_buf[:length])))
|
||||
return self.not_match_return(self.recv_buf)
|
||||
time_dif = common.int32(utc_time - (int(time.time()) & 0xffffffff))
|
||||
if time_dif < -self.max_time_dif or time_dif > self.max_time_dif:
|
||||
logging.info('%s: wrong timestamp, time_dif %d, data %s' % (self.no_compatible_method, time_dif, binascii.hexlify(head)))
|
||||
return self.not_match_return(self.recv_buf)
|
||||
elif self.server_info.data.insert(self.user_id, client_id, connection_id):
|
||||
self.has_recv_header = True
|
||||
out_buf = self.recv_buf[31 + rnd_len:length - 4]
|
||||
self.client_id = client_id
|
||||
self.connection_id = connection_id
|
||||
else:
|
||||
logging.info('%s: auth fail, data %s' % (self.no_compatible_method, binascii.hexlify(out_buf)))
|
||||
return self.not_match_return(self.recv_buf)
|
||||
self.recv_buf = self.recv_buf[length:]
|
||||
self.has_recv_header = True
|
||||
sendback = True
|
||||
|
||||
while len(self.recv_buf) > 4:
|
||||
mac_key = self.user_key + struct.pack('<I', self.recv_id)
|
||||
mac = hmac.new(mac_key, self.recv_buf[:2], self.hashfunc).digest()[:2]
|
||||
if mac != self.recv_buf[2:4]:
|
||||
self.raw_trans = True
|
||||
logging.info(self.no_compatible_method + ': wrong crc')
|
||||
if self.recv_id == 0:
|
||||
logging.info(self.no_compatible_method + ': wrong crc')
|
||||
return (b'E'*2048, False)
|
||||
else:
|
||||
raise Exception('server_post_decrype data error')
|
||||
length = struct.unpack('<H', self.recv_buf[:2])[0]
|
||||
if length >= 8192 or length < 7:
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
if self.recv_id == 0:
|
||||
logging.info(self.no_compatible_method + ': over size')
|
||||
return (b'E'*2048, False)
|
||||
else:
|
||||
raise Exception('server_post_decrype data error')
|
||||
if length > len(self.recv_buf):
|
||||
break
|
||||
|
||||
if hmac.new(mac_key, self.recv_buf[:length - 4], self.hashfunc).digest()[:4] != self.recv_buf[length - 4:length]:
|
||||
logging.info('%s: checksum error, data %s' % (self.no_compatible_method, binascii.hexlify(self.recv_buf[:length])))
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
if self.recv_id == 0:
|
||||
return (b'E'*2048, False)
|
||||
else:
|
||||
raise Exception('server_post_decrype data uncorrect checksum')
|
||||
|
||||
self.recv_id = (self.recv_id + 1) & 0xFFFFFFFF
|
||||
pos = common.ord(self.recv_buf[4])
|
||||
if pos < 255:
|
||||
pos += 4
|
||||
else:
|
||||
pos = struct.unpack('<H', self.recv_buf[5:7])[0] + 4
|
||||
out_buf += self.recv_buf[pos:length - 4]
|
||||
self.recv_buf = self.recv_buf[length:]
|
||||
if pos == length - 4:
|
||||
sendback = True
|
||||
|
||||
if out_buf:
|
||||
self.server_info.data.update(self.user_id, self.client_id, self.connection_id)
|
||||
return (out_buf, sendback)
|
||||
|
||||
def client_udp_pre_encrypt(self, buf):
|
||||
if self.user_key is None:
|
||||
if b':' in to_bytes(self.server_info.protocol_param):
|
||||
try:
|
||||
items = to_bytes(self.server_info.protocol_param).split(':')
|
||||
self.user_key = self.hashfunc(items[1]).digest()
|
||||
self.user_id = struct.pack('<I', int(items[0]))
|
||||
except:
|
||||
pass
|
||||
if self.user_key is None:
|
||||
self.user_id = os.urandom(4)
|
||||
self.user_key = self.server_info.key
|
||||
buf += self.user_id
|
||||
return buf + hmac.new(self.user_key, buf, self.hashfunc).digest()[:4]
|
||||
|
||||
def client_udp_post_decrypt(self, buf):
|
||||
user_key = self.server_info.key
|
||||
if hmac.new(user_key, buf[:-4], self.hashfunc).digest()[:4] != buf[-4:]:
|
||||
return b''
|
||||
return buf[:-4]
|
||||
|
||||
def server_udp_pre_encrypt(self, buf, uid):
|
||||
user_key = self.server_info.key
|
||||
return buf + hmac.new(user_key, buf, self.hashfunc).digest()[:4]
|
||||
|
||||
def server_udp_post_decrypt(self, buf):
|
||||
uid = buf[-8:-4]
|
||||
if uid in self.server_info.users:
|
||||
user_key = self.hashfunc(self.server_info.users[uid]).digest()
|
||||
else:
|
||||
uid = None
|
||||
if not self.server_info.users:
|
||||
user_key = self.server_info.key
|
||||
else:
|
||||
user_key = self.server_info.recv_iv
|
||||
if hmac.new(user_key, buf[:-4], self.hashfunc).digest()[:4] != buf[-4:]:
|
||||
return (b'', None)
|
||||
return (buf[:-8], uid)
|
||||
|
@ -0,0 +1,692 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2015-2015 breakwa11
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
import binascii
|
||||
import base64
|
||||
import time
|
||||
import datetime
|
||||
import random
|
||||
import math
|
||||
import struct
|
||||
import zlib
|
||||
import hmac
|
||||
import hashlib
|
||||
import bisect
|
||||
|
||||
import shadowsocks
|
||||
from shadowsocks import common, lru_cache, encrypt
|
||||
from shadowsocks.obfsplugin import plain
|
||||
from shadowsocks.common import to_bytes, to_str, ord, chr
|
||||
|
||||
def create_auth_chain_a(method):
|
||||
return auth_chain_a(method)
|
||||
|
||||
def create_auth_chain_b(method):
|
||||
return auth_chain_b(method)
|
||||
|
||||
obfs_map = {
|
||||
'auth_chain_a': (create_auth_chain_a,),
|
||||
'auth_chain_b': (create_auth_chain_b,),
|
||||
}
|
||||
|
||||
class xorshift128plus(object):
|
||||
max_int = (1 << 64) - 1
|
||||
mov_mask = (1 << (64 - 23)) - 1
|
||||
|
||||
def __init__(self):
|
||||
self.v0 = 0
|
||||
self.v1 = 0
|
||||
|
||||
def next(self):
|
||||
x = self.v0
|
||||
y = self.v1
|
||||
self.v0 = y
|
||||
x ^= ((x & xorshift128plus.mov_mask) << 23)
|
||||
x ^= (y ^ (x >> 17) ^ (y >> 26)) & xorshift128plus.max_int
|
||||
self.v1 = x
|
||||
return (x + y) & xorshift128plus.max_int
|
||||
|
||||
def init_from_bin(self, bin):
|
||||
bin += b'\0' * 16
|
||||
self.v0 = struct.unpack('<Q', bin[:8])[0]
|
||||
self.v1 = struct.unpack('<Q', bin[8:16])[0]
|
||||
|
||||
def init_from_bin_len(self, bin, length):
|
||||
bin += b'\0' * 16
|
||||
bin = struct.pack('<H', length) + bin[2:]
|
||||
self.v0 = struct.unpack('<Q', bin[:8])[0]
|
||||
self.v1 = struct.unpack('<Q', bin[8:16])[0]
|
||||
|
||||
for i in range(4):
|
||||
self.next()
|
||||
|
||||
def match_begin(str1, str2):
|
||||
if len(str1) >= len(str2):
|
||||
if str1[:len(str2)] == str2:
|
||||
return True
|
||||
return False
|
||||
|
||||
class auth_base(plain.plain):
|
||||
def __init__(self, method):
|
||||
super(auth_base, self).__init__(method)
|
||||
self.method = method
|
||||
self.no_compatible_method = ''
|
||||
self.overhead = 4
|
||||
|
||||
def init_data(self):
|
||||
return ''
|
||||
|
||||
def get_overhead(self, direction): # direction: true for c->s false for s->c
|
||||
return self.overhead
|
||||
|
||||
def set_server_info(self, server_info):
|
||||
self.server_info = server_info
|
||||
|
||||
def client_encode(self, buf):
|
||||
return buf
|
||||
|
||||
def client_decode(self, buf):
|
||||
return (buf, False)
|
||||
|
||||
def server_encode(self, buf):
|
||||
return buf
|
||||
|
||||
def server_decode(self, buf):
|
||||
return (buf, True, False)
|
||||
|
||||
def not_match_return(self, buf):
|
||||
self.raw_trans = True
|
||||
self.overhead = 0
|
||||
if self.method == self.no_compatible_method:
|
||||
return (b'E'*2048, False)
|
||||
return (buf, False)
|
||||
|
||||
class client_queue(object):
|
||||
def __init__(self, begin_id):
|
||||
self.front = begin_id - 64
|
||||
self.back = begin_id + 1
|
||||
self.alloc = {}
|
||||
self.enable = True
|
||||
self.last_update = time.time()
|
||||
self.ref = 0
|
||||
|
||||
def update(self):
|
||||
self.last_update = time.time()
|
||||
|
||||
def addref(self):
|
||||
self.ref += 1
|
||||
|
||||
def delref(self):
|
||||
if self.ref > 0:
|
||||
self.ref -= 1
|
||||
|
||||
def is_active(self):
|
||||
return (self.ref > 0) and (time.time() - self.last_update < 60 * 10)
|
||||
|
||||
def re_enable(self, connection_id):
|
||||
self.enable = True
|
||||
self.front = connection_id - 64
|
||||
self.back = connection_id + 1
|
||||
self.alloc = {}
|
||||
|
||||
def insert(self, connection_id):
|
||||
if not self.enable:
|
||||
logging.warn('obfs auth: not enable')
|
||||
return False
|
||||
if not self.is_active():
|
||||
self.re_enable(connection_id)
|
||||
self.update()
|
||||
if connection_id < self.front:
|
||||
logging.warn('obfs auth: deprecated id, someone replay attack')
|
||||
return False
|
||||
if connection_id > self.front + 0x4000:
|
||||
logging.warn('obfs auth: wrong id')
|
||||
return False
|
||||
if connection_id in self.alloc:
|
||||
logging.warn('obfs auth: duplicate id, someone replay attack')
|
||||
return False
|
||||
if self.back <= connection_id:
|
||||
self.back = connection_id + 1
|
||||
self.alloc[connection_id] = 1
|
||||
while (self.front in self.alloc) or self.front + 0x1000 < self.back:
|
||||
if self.front in self.alloc:
|
||||
del self.alloc[self.front]
|
||||
self.front += 1
|
||||
self.addref()
|
||||
return True
|
||||
|
||||
class obfs_auth_chain_data(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.user_id = {}
|
||||
self.local_client_id = b''
|
||||
self.connection_id = 0
|
||||
self.set_max_client(64) # max active client count
|
||||
|
||||
def update(self, user_id, client_id, connection_id):
|
||||
if user_id not in self.user_id:
|
||||
self.user_id[user_id] = lru_cache.LRUCache()
|
||||
local_client_id = self.user_id[user_id]
|
||||
|
||||
if client_id in local_client_id:
|
||||
local_client_id[client_id].update()
|
||||
|
||||
def set_max_client(self, max_client):
|
||||
self.max_client = max_client
|
||||
self.max_buffer = max(self.max_client * 2, 1024)
|
||||
|
||||
def insert(self, user_id, client_id, connection_id):
|
||||
if user_id not in self.user_id:
|
||||
self.user_id[user_id] = lru_cache.LRUCache()
|
||||
local_client_id = self.user_id[user_id]
|
||||
|
||||
if local_client_id.get(client_id, None) is None or not local_client_id[client_id].enable:
|
||||
if local_client_id.first() is None or len(local_client_id) < self.max_client:
|
||||
if client_id not in local_client_id:
|
||||
#TODO: check
|
||||
local_client_id[client_id] = client_queue(connection_id)
|
||||
else:
|
||||
local_client_id[client_id].re_enable(connection_id)
|
||||
return local_client_id[client_id].insert(connection_id)
|
||||
|
||||
if not local_client_id[local_client_id.first()].is_active():
|
||||
del local_client_id[local_client_id.first()]
|
||||
if client_id not in local_client_id:
|
||||
#TODO: check
|
||||
local_client_id[client_id] = client_queue(connection_id)
|
||||
else:
|
||||
local_client_id[client_id].re_enable(connection_id)
|
||||
return local_client_id[client_id].insert(connection_id)
|
||||
|
||||
logging.warn(self.name + ': no inactive client')
|
||||
return False
|
||||
else:
|
||||
return local_client_id[client_id].insert(connection_id)
|
||||
|
||||
def remove(self, user_id, client_id):
|
||||
if user_id in self.user_id:
|
||||
local_client_id = self.user_id[user_id]
|
||||
if client_id in local_client_id:
|
||||
local_client_id[client_id].delref()
|
||||
|
||||
class auth_chain_a(auth_base):
|
||||
def __init__(self, method):
|
||||
super(auth_chain_a, self).__init__(method)
|
||||
self.hashfunc = hashlib.md5
|
||||
self.recv_buf = b''
|
||||
self.unit_len = 2800
|
||||
self.raw_trans = False
|
||||
self.has_sent_header = False
|
||||
self.has_recv_header = False
|
||||
self.client_id = 0
|
||||
self.connection_id = 0
|
||||
self.max_time_dif = 60 * 60 * 24 # time dif (second) setting
|
||||
self.salt = b"auth_chain_a"
|
||||
self.no_compatible_method = 'auth_chain_a'
|
||||
self.pack_id = 1
|
||||
self.recv_id = 1
|
||||
self.user_id = None
|
||||
self.user_id_num = 0
|
||||
self.user_key = None
|
||||
self.overhead = 4
|
||||
self.client_over_head = 4
|
||||
self.last_client_hash = b''
|
||||
self.last_server_hash = b''
|
||||
self.random_client = xorshift128plus()
|
||||
self.random_server = xorshift128plus()
|
||||
self.encryptor = None
|
||||
|
||||
def init_data(self):
|
||||
return obfs_auth_chain_data(self.method)
|
||||
|
||||
def get_overhead(self, direction): # direction: true for c->s false for s->c
|
||||
return self.overhead
|
||||
|
||||
def set_server_info(self, server_info):
|
||||
self.server_info = server_info
|
||||
try:
|
||||
max_client = int(server_info.protocol_param.split('#')[0])
|
||||
except:
|
||||
max_client = 64
|
||||
self.server_info.data.set_max_client(max_client)
|
||||
|
||||
def trapezoid_random_float(self, d):
|
||||
if d == 0:
|
||||
return random.random()
|
||||
s = random.random()
|
||||
a = 1 - d
|
||||
return (math.sqrt(a * a + 4 * d * s) - a) / (2 * d)
|
||||
|
||||
def trapezoid_random_int(self, max_val, d):
|
||||
v = self.trapezoid_random_float(d)
|
||||
return int(v * max_val)
|
||||
|
||||
def rnd_data_len(self, buf_size, last_hash, random):
|
||||
if buf_size > 1440:
|
||||
return 0
|
||||
random.init_from_bin_len(last_hash, buf_size)
|
||||
if buf_size > 1300:
|
||||
return random.next() % 31
|
||||
if buf_size > 900:
|
||||
return random.next() % 127
|
||||
if buf_size > 400:
|
||||
return random.next() % 521
|
||||
return random.next() % 1021
|
||||
|
||||
def udp_rnd_data_len(self, last_hash, random):
|
||||
random.init_from_bin(last_hash)
|
||||
return random.next() % 127
|
||||
|
||||
def rnd_start_pos(self, rand_len, random):
|
||||
if rand_len > 0:
|
||||
return random.next() % 8589934609 % rand_len
|
||||
return 0
|
||||
|
||||
def rnd_data(self, buf_size, buf, last_hash, random):
|
||||
rand_len = self.rnd_data_len(buf_size, last_hash, random)
|
||||
|
||||
rnd_data_buf = os.urandom(rand_len)
|
||||
|
||||
if buf_size == 0:
|
||||
return rnd_data_buf
|
||||
else:
|
||||
if rand_len > 0:
|
||||
start_pos = self.rnd_start_pos(rand_len, random)
|
||||
return rnd_data_buf[:start_pos] + buf + rnd_data_buf[start_pos:]
|
||||
else:
|
||||
return buf
|
||||
|
||||
def pack_client_data(self, buf):
|
||||
buf = self.encryptor.encrypt(buf)
|
||||
data = self.rnd_data(len(buf), buf, self.last_client_hash, self.random_client)
|
||||
data_len = len(data) + 8
|
||||
mac_key = self.user_key + struct.pack('<I', self.pack_id)
|
||||
length = len(buf) ^ struct.unpack('<H', self.last_client_hash[14:])[0]
|
||||
data = struct.pack('<H', length) + data
|
||||
self.last_client_hash = hmac.new(mac_key, data, self.hashfunc).digest()
|
||||
data += self.last_client_hash[:2]
|
||||
self.pack_id = (self.pack_id + 1) & 0xFFFFFFFF
|
||||
return data
|
||||
|
||||
def pack_server_data(self, buf):
|
||||
buf = self.encryptor.encrypt(buf)
|
||||
data = self.rnd_data(len(buf), buf, self.last_server_hash, self.random_server)
|
||||
data_len = len(data) + 8
|
||||
mac_key = self.user_key + struct.pack('<I', self.pack_id)
|
||||
length = len(buf) ^ struct.unpack('<H', self.last_server_hash[14:])[0]
|
||||
data = struct.pack('<H', length) + data
|
||||
self.last_server_hash = hmac.new(mac_key, data, self.hashfunc).digest()
|
||||
data += self.last_server_hash[:2]
|
||||
self.pack_id = (self.pack_id + 1) & 0xFFFFFFFF
|
||||
return data
|
||||
|
||||
def pack_auth_data(self, auth_data, buf):
|
||||
data = auth_data
|
||||
data_len = 12 + 4 + 16 + 4
|
||||
data = data + (struct.pack('<H', self.server_info.overhead) + struct.pack('<H', 0))
|
||||
mac_key = self.server_info.iv + self.server_info.key
|
||||
|
||||
check_head = os.urandom(4)
|
||||
self.last_client_hash = hmac.new(mac_key, check_head, self.hashfunc).digest()
|
||||
check_head += self.last_client_hash[:8]
|
||||
|
||||
if b':' in to_bytes(self.server_info.protocol_param):
|
||||
try:
|
||||
items = to_bytes(self.server_info.protocol_param).split(b':')
|
||||
self.user_key = items[1]
|
||||
uid = struct.pack('<I', int(items[0]))
|
||||
except:
|
||||
uid = os.urandom(4)
|
||||
else:
|
||||
uid = os.urandom(4)
|
||||
if self.user_key is None:
|
||||
self.user_key = self.server_info.key
|
||||
|
||||
encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(self.user_key)) + self.salt, 'aes-128-cbc', b'\x00' * 16)
|
||||
|
||||
uid = struct.unpack('<I', uid)[0] ^ struct.unpack('<I', self.last_client_hash[8:12])[0]
|
||||
uid = struct.pack('<I', uid)
|
||||
data = uid + encryptor.encrypt(data)[16:]
|
||||
self.last_server_hash = hmac.new(self.user_key, data, self.hashfunc).digest()
|
||||
data = check_head + data + self.last_server_hash[:4]
|
||||
self.encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(self.user_key)) + to_bytes(base64.b64encode(self.last_client_hash)), 'rc4')
|
||||
return data + self.pack_client_data(buf)
|
||||
|
||||
def auth_data(self):
|
||||
utc_time = int(time.time()) & 0xFFFFFFFF
|
||||
if self.server_info.data.connection_id > 0xFF000000:
|
||||
self.server_info.data.local_client_id = b''
|
||||
if not self.server_info.data.local_client_id:
|
||||
self.server_info.data.local_client_id = os.urandom(4)
|
||||
logging.debug("local_client_id %s" % (binascii.hexlify(self.server_info.data.local_client_id),))
|
||||
self.server_info.data.connection_id = struct.unpack('<I', os.urandom(4))[0] & 0xFFFFFF
|
||||
self.server_info.data.connection_id += 1
|
||||
return b''.join([struct.pack('<I', utc_time),
|
||||
self.server_info.data.local_client_id,
|
||||
struct.pack('<I', self.server_info.data.connection_id)])
|
||||
|
||||
def client_pre_encrypt(self, buf):
|
||||
ret = b''
|
||||
ogn_data_len = len(buf)
|
||||
if not self.has_sent_header:
|
||||
head_size = self.get_head_size(buf, 30)
|
||||
datalen = min(len(buf), random.randint(0, 31) + head_size)
|
||||
ret += self.pack_auth_data(self.auth_data(), buf[:datalen])
|
||||
buf = buf[datalen:]
|
||||
self.has_sent_header = True
|
||||
while len(buf) > self.unit_len:
|
||||
ret += self.pack_client_data(buf[:self.unit_len])
|
||||
buf = buf[self.unit_len:]
|
||||
ret += self.pack_client_data(buf)
|
||||
return ret
|
||||
|
||||
def client_post_decrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return buf
|
||||
self.recv_buf += buf
|
||||
out_buf = b''
|
||||
while len(self.recv_buf) > 4:
|
||||
mac_key = self.user_key + struct.pack('<I', self.recv_id)
|
||||
data_len = struct.unpack('<H', self.recv_buf[:2])[0] ^ struct.unpack('<H', self.last_server_hash[14:16])[0]
|
||||
rand_len = self.rnd_data_len(data_len, self.last_server_hash, self.random_server)
|
||||
length = data_len + rand_len
|
||||
if length >= 4096:
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
raise Exception('client_post_decrypt data error')
|
||||
|
||||
if length + 4 > len(self.recv_buf):
|
||||
break
|
||||
|
||||
server_hash = hmac.new(mac_key, self.recv_buf[:length + 2], self.hashfunc).digest()
|
||||
if server_hash[:2] != self.recv_buf[length + 2 : length + 4]:
|
||||
logging.info('%s: checksum error, data %s' % (self.no_compatible_method, binascii.hexlify(self.recv_buf[:length])))
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
raise Exception('client_post_decrypt data uncorrect checksum')
|
||||
|
||||
pos = 2
|
||||
if data_len > 0 and rand_len > 0:
|
||||
pos = 2 + self.rnd_start_pos(rand_len, self.random_server)
|
||||
out_buf += self.encryptor.decrypt(self.recv_buf[pos : data_len + pos])
|
||||
self.last_server_hash = server_hash
|
||||
if self.recv_id == 1:
|
||||
self.server_info.tcp_mss = struct.unpack('<H', out_buf[:2])[0]
|
||||
out_buf = out_buf[2:]
|
||||
self.recv_id = (self.recv_id + 1) & 0xFFFFFFFF
|
||||
self.recv_buf = self.recv_buf[length + 4:]
|
||||
|
||||
return out_buf
|
||||
|
||||
def server_pre_encrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return buf
|
||||
ret = b''
|
||||
if self.pack_id == 1:
|
||||
tcp_mss = self.server_info.tcp_mss if self.server_info.tcp_mss < 1500 else 1500
|
||||
self.server_info.tcp_mss = tcp_mss
|
||||
buf = struct.pack('<H', tcp_mss) + buf
|
||||
self.unit_len = tcp_mss - self.client_over_head
|
||||
while len(buf) > self.unit_len:
|
||||
ret += self.pack_server_data(buf[:self.unit_len])
|
||||
buf = buf[self.unit_len:]
|
||||
ret += self.pack_server_data(buf)
|
||||
return ret
|
||||
|
||||
def server_post_decrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return (buf, False)
|
||||
self.recv_buf += buf
|
||||
out_buf = b''
|
||||
sendback = False
|
||||
|
||||
if not self.has_recv_header:
|
||||
if len(self.recv_buf) >= 12 or len(self.recv_buf) in [7, 8]:
|
||||
recv_len = min(len(self.recv_buf), 12)
|
||||
mac_key = self.server_info.recv_iv + self.server_info.key
|
||||
md5data = hmac.new(mac_key, self.recv_buf[:4], self.hashfunc).digest()
|
||||
if md5data[:recv_len - 4] != self.recv_buf[4:recv_len]:
|
||||
return self.not_match_return(self.recv_buf)
|
||||
|
||||
if len(self.recv_buf) < 12 + 24:
|
||||
return (b'', False)
|
||||
|
||||
self.last_client_hash = md5data
|
||||
uid = struct.unpack('<I', self.recv_buf[12:16])[0] ^ struct.unpack('<I', md5data[8:12])[0]
|
||||
self.user_id_num = uid
|
||||
uid = struct.pack('<I', uid)
|
||||
if uid in self.server_info.users:
|
||||
self.user_id = uid
|
||||
self.user_key = self.server_info.users[uid]
|
||||
self.server_info.update_user_func(uid)
|
||||
else:
|
||||
self.user_id_num = 0
|
||||
if not self.server_info.users:
|
||||
self.user_key = self.server_info.key
|
||||
else:
|
||||
self.user_key = self.server_info.recv_iv
|
||||
|
||||
md5data = hmac.new(self.user_key, self.recv_buf[12 : 12 + 20], self.hashfunc).digest()
|
||||
if md5data[:4] != self.recv_buf[32:36]:
|
||||
logging.error('%s data uncorrect auth HMAC-MD5 from %s:%d, data %s' % (self.no_compatible_method, self.server_info.client, self.server_info.client_port, binascii.hexlify(self.recv_buf)))
|
||||
if len(self.recv_buf) < 36:
|
||||
return (b'', False)
|
||||
return self.not_match_return(self.recv_buf)
|
||||
|
||||
self.last_server_hash = md5data
|
||||
encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(self.user_key)) + self.salt, 'aes-128-cbc')
|
||||
head = encryptor.decrypt(b'\x00' * 16 + self.recv_buf[16:32] + b'\x00') # need an extra byte or recv empty
|
||||
self.client_over_head = struct.unpack('<H', head[12:14])[0]
|
||||
|
||||
utc_time = struct.unpack('<I', head[:4])[0]
|
||||
client_id = struct.unpack('<I', head[4:8])[0]
|
||||
connection_id = struct.unpack('<I', head[8:12])[0]
|
||||
time_dif = common.int32(utc_time - (int(time.time()) & 0xffffffff))
|
||||
if time_dif < -self.max_time_dif or time_dif > self.max_time_dif:
|
||||
logging.info('%s: wrong timestamp, time_dif %d, data %s' % (self.no_compatible_method, time_dif, binascii.hexlify(head)))
|
||||
return self.not_match_return(self.recv_buf)
|
||||
elif self.server_info.data.insert(self.user_id, client_id, connection_id):
|
||||
self.has_recv_header = True
|
||||
self.client_id = client_id
|
||||
self.connection_id = connection_id
|
||||
else:
|
||||
logging.info('%s: auth fail, data %s' % (self.no_compatible_method, binascii.hexlify(out_buf)))
|
||||
return self.not_match_return(self.recv_buf)
|
||||
|
||||
self.encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(self.user_key)) + to_bytes(base64.b64encode(self.last_client_hash)), 'rc4')
|
||||
self.recv_buf = self.recv_buf[36:]
|
||||
self.has_recv_header = True
|
||||
sendback = True
|
||||
|
||||
while len(self.recv_buf) > 4:
|
||||
mac_key = self.user_key + struct.pack('<I', self.recv_id)
|
||||
data_len = struct.unpack('<H', self.recv_buf[:2])[0] ^ struct.unpack('<H', self.last_client_hash[14:16])[0]
|
||||
rand_len = self.rnd_data_len(data_len, self.last_client_hash, self.random_client)
|
||||
length = data_len + rand_len
|
||||
if length >= 4096:
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
if self.recv_id == 0:
|
||||
logging.info(self.no_compatible_method + ': over size')
|
||||
return (b'E'*2048, False)
|
||||
else:
|
||||
raise Exception('server_post_decrype data error')
|
||||
|
||||
if length + 4 > len(self.recv_buf):
|
||||
break
|
||||
|
||||
client_hash = hmac.new(mac_key, self.recv_buf[:length + 2], self.hashfunc).digest()
|
||||
if client_hash[:2] != self.recv_buf[length + 2 : length + 4]:
|
||||
logging.info('%s: checksum error, data %s' % (self.no_compatible_method, binascii.hexlify(self.recv_buf[:length])))
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
if self.recv_id == 0:
|
||||
return (b'E'*2048, False)
|
||||
else:
|
||||
raise Exception('server_post_decrype data uncorrect checksum')
|
||||
|
||||
self.recv_id = (self.recv_id + 1) & 0xFFFFFFFF
|
||||
pos = 2
|
||||
if data_len > 0 and rand_len > 0:
|
||||
pos = 2 + self.rnd_start_pos(rand_len, self.random_client)
|
||||
out_buf += self.encryptor.decrypt(self.recv_buf[pos : data_len + pos])
|
||||
self.last_client_hash = client_hash
|
||||
self.recv_buf = self.recv_buf[length + 4:]
|
||||
if data_len == 0:
|
||||
sendback = True
|
||||
|
||||
if out_buf:
|
||||
self.server_info.data.update(self.user_id, self.client_id, self.connection_id)
|
||||
return (out_buf, sendback)
|
||||
|
||||
def client_udp_pre_encrypt(self, buf):
|
||||
if self.user_key is None:
|
||||
if b':' in to_bytes(self.server_info.protocol_param):
|
||||
try:
|
||||
items = to_bytes(self.server_info.protocol_param).split(':')
|
||||
self.user_key = self.hashfunc(items[1]).digest()
|
||||
self.user_id = struct.pack('<I', int(items[0]))
|
||||
except:
|
||||
pass
|
||||
if self.user_key is None:
|
||||
self.user_id = os.urandom(4)
|
||||
self.user_key = self.server_info.key
|
||||
authdata = os.urandom(3)
|
||||
mac_key = self.server_info.key
|
||||
md5data = hmac.new(mac_key, authdata, self.hashfunc).digest()
|
||||
uid = struct.unpack('<I', self.user_id)[0] ^ struct.unpack('<I', md5data[:4])[0]
|
||||
uid = struct.pack('<I', uid)
|
||||
rand_len = self.udp_rnd_data_len(md5data, self.random_client)
|
||||
encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(self.user_key)) + to_bytes(base64.b64encode(md5data)), 'rc4')
|
||||
out_buf = encryptor.encrypt(buf)
|
||||
buf = out_buf + os.urandom(rand_len) + authdata + uid
|
||||
return buf + hmac.new(self.user_key, buf, self.hashfunc).digest()[:1]
|
||||
|
||||
def client_udp_post_decrypt(self, buf):
|
||||
if len(buf) <= 8:
|
||||
return (b'', None)
|
||||
if hmac.new(self.user_key, buf[:-1], self.hashfunc).digest()[:1] != buf[-1:]:
|
||||
return (b'', None)
|
||||
mac_key = self.server_info.key
|
||||
md5data = hmac.new(mac_key, buf[-8:-1], self.hashfunc).digest()
|
||||
rand_len = self.udp_rnd_data_len(md5data, self.random_server)
|
||||
encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(self.user_key)) + to_bytes(base64.b64encode(md5data)), 'rc4')
|
||||
return encryptor.decrypt(buf[:-8 - rand_len])
|
||||
|
||||
def server_udp_pre_encrypt(self, buf, uid):
|
||||
if uid in self.server_info.users:
|
||||
user_key = self.server_info.users[uid]
|
||||
else:
|
||||
uid = None
|
||||
if not self.server_info.users:
|
||||
user_key = self.server_info.key
|
||||
else:
|
||||
user_key = self.server_info.recv_iv
|
||||
authdata = os.urandom(7)
|
||||
mac_key = self.server_info.key
|
||||
md5data = hmac.new(mac_key, authdata, self.hashfunc).digest()
|
||||
rand_len = self.udp_rnd_data_len(md5data, self.random_server)
|
||||
encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(user_key)) + to_bytes(base64.b64encode(md5data)), 'rc4')
|
||||
out_buf = encryptor.encrypt(buf)
|
||||
buf = out_buf + os.urandom(rand_len) + authdata
|
||||
return buf + hmac.new(user_key, buf, self.hashfunc).digest()[:1]
|
||||
|
||||
def server_udp_post_decrypt(self, buf):
|
||||
mac_key = self.server_info.key
|
||||
md5data = hmac.new(mac_key, buf[-8:-5], self.hashfunc).digest()
|
||||
uid = struct.unpack('<I', buf[-5:-1])[0] ^ struct.unpack('<I', md5data[:4])[0]
|
||||
uid = struct.pack('<I', uid)
|
||||
if uid in self.server_info.users:
|
||||
user_key = self.server_info.users[uid]
|
||||
else:
|
||||
uid = None
|
||||
if not self.server_info.users:
|
||||
user_key = self.server_info.key
|
||||
else:
|
||||
user_key = self.server_info.recv_iv
|
||||
if hmac.new(user_key, buf[:-1], self.hashfunc).digest()[:1] != buf[-1:]:
|
||||
return (b'', None)
|
||||
rand_len = self.udp_rnd_data_len(md5data, self.random_client)
|
||||
encryptor = encrypt.Encryptor(to_bytes(base64.b64encode(user_key)) + to_bytes(base64.b64encode(md5data)), 'rc4')
|
||||
out_buf = encryptor.decrypt(buf[:-8 - rand_len])
|
||||
return (out_buf, uid)
|
||||
|
||||
def dispose(self):
|
||||
self.server_info.data.remove(self.user_id, self.client_id)
|
||||
|
||||
class auth_chain_b(auth_chain_a):
|
||||
def __init__(self, method):
|
||||
super(auth_chain_b, self).__init__(method)
|
||||
self.salt = b"auth_chain_b"
|
||||
self.no_compatible_method = 'auth_chain_b'
|
||||
self.data_size_list = []
|
||||
self.data_size_list2 = []
|
||||
|
||||
def init_data_size(self, key):
|
||||
if self.data_size_list:
|
||||
self.data_size_list = []
|
||||
self.data_size_list2 = []
|
||||
random = xorshift128plus()
|
||||
random.init_from_bin(key)
|
||||
list_len = random.next() % 8 + 4
|
||||
for i in range(0, list_len):
|
||||
self.data_size_list.append((int)(random.next() % 2340 % 2040 % 1440))
|
||||
self.data_size_list.sort()
|
||||
list_len = random.next() % 16 + 8
|
||||
for i in range(0, list_len):
|
||||
self.data_size_list2.append((int)(random.next() % 2340 % 2040 % 1440))
|
||||
self.data_size_list2.sort()
|
||||
|
||||
def set_server_info(self, server_info):
|
||||
self.server_info = server_info
|
||||
try:
|
||||
max_client = int(server_info.protocol_param.split('#')[0])
|
||||
except:
|
||||
max_client = 64
|
||||
self.server_info.data.set_max_client(max_client)
|
||||
self.init_data_size(self.server_info.key)
|
||||
|
||||
def rnd_data_len(self, buf_size, last_hash, random):
|
||||
if buf_size >= 1440:
|
||||
return 0
|
||||
random.init_from_bin_len(last_hash, buf_size)
|
||||
pos = bisect.bisect_left(self.data_size_list, buf_size + self.server_info.overhead)
|
||||
final_pos = pos + random.next() % (len(self.data_size_list))
|
||||
if final_pos < len(self.data_size_list):
|
||||
return self.data_size_list[final_pos] - buf_size - self.server_info.overhead
|
||||
|
||||
pos = bisect.bisect_left(self.data_size_list2, buf_size + self.server_info.overhead)
|
||||
final_pos = pos + random.next() % (len(self.data_size_list2))
|
||||
if final_pos < len(self.data_size_list2):
|
||||
return self.data_size_list2[final_pos] - buf_size - self.server_info.overhead
|
||||
if final_pos < pos + len(self.data_size_list2) - 1:
|
||||
return 0
|
||||
|
||||
if buf_size > 1300:
|
||||
return random.next() % 31
|
||||
if buf_size > 900:
|
||||
return random.next() % 127
|
||||
if buf_size > 400:
|
||||
return random.next() % 521
|
||||
return random.next() % 1021
|
||||
|
@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2015-2015 breakwa11
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
import binascii
|
||||
import struct
|
||||
import base64
|
||||
import datetime
|
||||
import random
|
||||
|
||||
from shadowsocks import common
|
||||
from shadowsocks.obfsplugin import plain
|
||||
from shadowsocks.common import to_bytes, to_str, ord, chr
|
||||
|
||||
def create_http_simple_obfs(method):
|
||||
return http_simple(method)
|
||||
|
||||
def create_http_post_obfs(method):
|
||||
return http_post(method)
|
||||
|
||||
def create_random_head_obfs(method):
|
||||
return random_head(method)
|
||||
|
||||
obfs_map = {
|
||||
'http_simple': (create_http_simple_obfs,),
|
||||
'http_simple_compatible': (create_http_simple_obfs,),
|
||||
'http_post': (create_http_post_obfs,),
|
||||
'http_post_compatible': (create_http_post_obfs,),
|
||||
'random_head': (create_random_head_obfs,),
|
||||
'random_head_compatible': (create_random_head_obfs,),
|
||||
}
|
||||
|
||||
def match_begin(str1, str2):
|
||||
if len(str1) >= len(str2):
|
||||
if str1[:len(str2)] == str2:
|
||||
return True
|
||||
return False
|
||||
|
||||
class http_simple(plain.plain):
|
||||
def __init__(self, method):
|
||||
self.method = method
|
||||
self.has_sent_header = False
|
||||
self.has_recv_header = False
|
||||
self.host = None
|
||||
self.port = 0
|
||||
self.recv_buffer = b''
|
||||
self.user_agent = [b"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0",
|
||||
b"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/44.0",
|
||||
b"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
|
||||
b"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/27.0.1453.93 Chrome/27.0.1453.93 Safari/537.36",
|
||||
b"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0",
|
||||
b"Mozilla/5.0 (compatible; WOW64; MSIE 10.0; Windows NT 6.2)",
|
||||
b"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27",
|
||||
b"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0; .NET4.0E; .NET4.0C)",
|
||||
b"Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
|
||||
b"Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/BuildID) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36",
|
||||
b"Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3",
|
||||
b"Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3"]
|
||||
|
||||
def encode_head(self, buf):
|
||||
hexstr = binascii.hexlify(buf)
|
||||
chs = []
|
||||
for i in range(0, len(hexstr), 2):
|
||||
chs.append(b"%" + hexstr[i:i+2])
|
||||
return b''.join(chs)
|
||||
|
||||
def client_encode(self, buf):
|
||||
if self.has_sent_header:
|
||||
return buf
|
||||
head_size = len(self.server_info.iv) + self.server_info.head_len
|
||||
if len(buf) - head_size > 64:
|
||||
headlen = head_size + random.randint(0, 64)
|
||||
else:
|
||||
headlen = len(buf)
|
||||
headdata = buf[:headlen]
|
||||
buf = buf[headlen:]
|
||||
port = b''
|
||||
if self.server_info.port != 80:
|
||||
port = b':' + to_bytes(str(self.server_info.port))
|
||||
body = None
|
||||
hosts = (self.server_info.obfs_param or self.server_info.host)
|
||||
pos = hosts.find("#")
|
||||
if pos >= 0:
|
||||
body = hosts[pos + 1:].replace("\n", "\r\n")
|
||||
body = body.replace("\\n", "\r\n")
|
||||
hosts = hosts[:pos]
|
||||
hosts = hosts.split(',')
|
||||
host = random.choice(hosts)
|
||||
http_head = b"GET /" + self.encode_head(headdata) + b" HTTP/1.1\r\n"
|
||||
http_head += b"Host: " + to_bytes(host) + port + b"\r\n"
|
||||
if body:
|
||||
http_head += body + "\r\n\r\n"
|
||||
else:
|
||||
http_head += b"User-Agent: " + random.choice(self.user_agent) + b"\r\n"
|
||||
http_head += b"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\nDNT: 1\r\nConnection: keep-alive\r\n\r\n"
|
||||
self.has_sent_header = True
|
||||
return http_head + buf
|
||||
|
||||
def client_decode(self, buf):
|
||||
if self.has_recv_header:
|
||||
return (buf, False)
|
||||
pos = buf.find(b'\r\n\r\n')
|
||||
if pos >= 0:
|
||||
self.has_recv_header = True
|
||||
return (buf[pos + 4:], False)
|
||||
else:
|
||||
return (b'', False)
|
||||
|
||||
def server_encode(self, buf):
|
||||
if self.has_sent_header:
|
||||
return buf
|
||||
|
||||
header = b'HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Encoding: gzip\r\nContent-Type: text/html\r\nDate: '
|
||||
header += to_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT'))
|
||||
header += b'\r\nServer: nginx\r\nVary: Accept-Encoding\r\n\r\n'
|
||||
self.has_sent_header = True
|
||||
return header + buf
|
||||
|
||||
def get_data_from_http_header(self, buf):
|
||||
ret_buf = b''
|
||||
lines = buf.split(b'\r\n')
|
||||
if lines and len(lines) > 1:
|
||||
hex_items = lines[0].split(b'%')
|
||||
if hex_items and len(hex_items) > 1:
|
||||
for index in range(1, len(hex_items)):
|
||||
if len(hex_items[index]) < 2:
|
||||
ret_buf += binascii.unhexlify('0' + hex_items[index])
|
||||
break
|
||||
elif len(hex_items[index]) > 2:
|
||||
ret_buf += binascii.unhexlify(hex_items[index][:2])
|
||||
break
|
||||
else:
|
||||
ret_buf += binascii.unhexlify(hex_items[index])
|
||||
return ret_buf
|
||||
return b''
|
||||
|
||||
def get_host_from_http_header(self, buf):
|
||||
ret_buf = b''
|
||||
lines = buf.split(b'\r\n')
|
||||
if lines and len(lines) > 1:
|
||||
for line in lines:
|
||||
if match_begin(line, b"Host: "):
|
||||
return common.to_str(line[6:])
|
||||
|
||||
def not_match_return(self, buf):
|
||||
self.has_sent_header = True
|
||||
self.has_recv_header = True
|
||||
if self.method == 'http_simple':
|
||||
return (b'E'*2048, False, False)
|
||||
return (buf, True, False)
|
||||
|
||||
def error_return(self, buf):
|
||||
self.has_sent_header = True
|
||||
self.has_recv_header = True
|
||||
return (b'E'*2048, False, False)
|
||||
|
||||
def server_decode(self, buf):
|
||||
if self.has_recv_header:
|
||||
return (buf, True, False)
|
||||
|
||||
self.recv_buffer += buf
|
||||
buf = self.recv_buffer
|
||||
if len(buf) > 10:
|
||||
if match_begin(buf, b'GET ') or match_begin(buf, b'POST '):
|
||||
if len(buf) > 65536:
|
||||
self.recv_buffer = None
|
||||
logging.warn('http_simple: over size')
|
||||
return self.not_match_return(buf)
|
||||
else: #not http header, run on original protocol
|
||||
self.recv_buffer = None
|
||||
logging.debug('http_simple: not match begin')
|
||||
return self.not_match_return(buf)
|
||||
else:
|
||||
return (b'', True, False)
|
||||
|
||||
if b'\r\n\r\n' in buf:
|
||||
datas = buf.split(b'\r\n\r\n', 1)
|
||||
ret_buf = self.get_data_from_http_header(buf)
|
||||
host = self.get_host_from_http_header(buf)
|
||||
if host and self.server_info.obfs_param:
|
||||
pos = host.find(":")
|
||||
if pos >= 0:
|
||||
host = host[:pos]
|
||||
hosts = self.server_info.obfs_param.split(',')
|
||||
if host not in hosts:
|
||||
return self.not_match_return(buf)
|
||||
if len(ret_buf) < 4:
|
||||
return self.error_return(buf)
|
||||
if len(datas) > 1:
|
||||
ret_buf += datas[1]
|
||||
if len(ret_buf) >= 13:
|
||||
self.has_recv_header = True
|
||||
return (ret_buf, True, False)
|
||||
return self.not_match_return(buf)
|
||||
else:
|
||||
return (b'', True, False)
|
||||
|
||||
class http_post(http_simple):
|
||||
def __init__(self, method):
|
||||
super(http_post, self).__init__(method)
|
||||
|
||||
def boundary(self):
|
||||
return to_bytes(''.join([random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") for i in range(32)]))
|
||||
|
||||
def client_encode(self, buf):
|
||||
if self.has_sent_header:
|
||||
return buf
|
||||
head_size = len(self.server_info.iv) + self.server_info.head_len
|
||||
if len(buf) - head_size > 64:
|
||||
headlen = head_size + random.randint(0, 64)
|
||||
else:
|
||||
headlen = len(buf)
|
||||
headdata = buf[:headlen]
|
||||
buf = buf[headlen:]
|
||||
port = b''
|
||||
if self.server_info.port != 80:
|
||||
port = b':' + to_bytes(str(self.server_info.port))
|
||||
body = None
|
||||
hosts = (self.server_info.obfs_param or self.server_info.host)
|
||||
pos = hosts.find("#")
|
||||
if pos >= 0:
|
||||
body = hosts[pos + 1:].replace("\\n", "\r\n")
|
||||
hosts = hosts[:pos]
|
||||
hosts = hosts.split(',')
|
||||
host = random.choice(hosts)
|
||||
http_head = b"POST /" + self.encode_head(headdata) + b" HTTP/1.1\r\n"
|
||||
http_head += b"Host: " + to_bytes(host) + port + b"\r\n"
|
||||
if body:
|
||||
http_head += body + "\r\n\r\n"
|
||||
else:
|
||||
http_head += b"User-Agent: " + random.choice(self.user_agent) + b"\r\n"
|
||||
http_head += b"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\n"
|
||||
http_head += b"Content-Type: multipart/form-data; boundary=" + self.boundary() + b"\r\nDNT: 1\r\n"
|
||||
http_head += b"Connection: keep-alive\r\n\r\n"
|
||||
self.has_sent_header = True
|
||||
return http_head + buf
|
||||
|
||||
def not_match_return(self, buf):
|
||||
self.has_sent_header = True
|
||||
self.has_recv_header = True
|
||||
if self.method == 'http_post':
|
||||
return (b'E'*2048, False, False)
|
||||
return (buf, True, False)
|
||||
|
||||
class random_head(plain.plain):
|
||||
def __init__(self, method):
|
||||
self.method = method
|
||||
self.has_sent_header = False
|
||||
self.has_recv_header = False
|
||||
self.raw_trans_sent = False
|
||||
self.raw_trans_recv = False
|
||||
self.send_buffer = b''
|
||||
|
||||
def client_encode(self, buf):
|
||||
if self.raw_trans_sent:
|
||||
return buf
|
||||
self.send_buffer += buf
|
||||
if not self.has_sent_header:
|
||||
self.has_sent_header = True
|
||||
data = os.urandom(common.ord(os.urandom(1)[0]) % 96 + 4)
|
||||
crc = (0xffffffff - binascii.crc32(data)) & 0xffffffff
|
||||
return data + struct.pack('<I', crc)
|
||||
if self.raw_trans_recv:
|
||||
ret = self.send_buffer
|
||||
self.send_buffer = b''
|
||||
self.raw_trans_sent = True
|
||||
return ret
|
||||
return b''
|
||||
|
||||
def client_decode(self, buf):
|
||||
if self.raw_trans_recv:
|
||||
return (buf, False)
|
||||
self.raw_trans_recv = True
|
||||
return (b'', True)
|
||||
|
||||
def server_encode(self, buf):
|
||||
if self.has_sent_header:
|
||||
return buf
|
||||
self.has_sent_header = True
|
||||
return os.urandom(common.ord(os.urandom(1)[0]) % 96 + 4)
|
||||
|
||||
def server_decode(self, buf):
|
||||
if self.has_recv_header:
|
||||
return (buf, True, False)
|
||||
|
||||
self.has_recv_header = True
|
||||
crc = binascii.crc32(buf) & 0xffffffff
|
||||
if crc != 0xffffffff:
|
||||
self.has_sent_header = True
|
||||
if self.method == 'random_head':
|
||||
return (b'E'*2048, False, False)
|
||||
return (buf, True, False)
|
||||
# (buffer_to_recv, is_need_decrypt, is_need_to_encode_and_send_back)
|
||||
return (b'', False, True)
|
||||
|
@ -0,0 +1,305 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2015-2015 breakwa11
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
import binascii
|
||||
import struct
|
||||
import base64
|
||||
import time
|
||||
import random
|
||||
import hmac
|
||||
import hashlib
|
||||
import string
|
||||
|
||||
from shadowsocks import common
|
||||
from shadowsocks.obfsplugin import plain
|
||||
from shadowsocks.common import to_bytes, to_str, ord
|
||||
from shadowsocks import lru_cache
|
||||
|
||||
def create_tls_ticket_auth_obfs(method):
|
||||
return tls_ticket_auth(method)
|
||||
|
||||
obfs_map = {
|
||||
'tls1.2_ticket_auth': (create_tls_ticket_auth_obfs,),
|
||||
'tls1.2_ticket_auth_compatible': (create_tls_ticket_auth_obfs,),
|
||||
'tls1.2_ticket_fastauth': (create_tls_ticket_auth_obfs,),
|
||||
'tls1.2_ticket_fastauth_compatible': (create_tls_ticket_auth_obfs,),
|
||||
}
|
||||
|
||||
def match_begin(str1, str2):
|
||||
if len(str1) >= len(str2):
|
||||
if str1[:len(str2)] == str2:
|
||||
return True
|
||||
return False
|
||||
|
||||
class obfs_auth_data(object):
|
||||
def __init__(self):
|
||||
self.client_data = lru_cache.LRUCache(60 * 5)
|
||||
self.client_id = os.urandom(32)
|
||||
self.startup_time = int(time.time() - 60 * 30) & 0xFFFFFFFF
|
||||
self.ticket_buf = {}
|
||||
|
||||
class tls_ticket_auth(plain.plain):
|
||||
def __init__(self, method):
|
||||
self.method = method
|
||||
self.handshake_status = 0
|
||||
self.send_buffer = b''
|
||||
self.recv_buffer = b''
|
||||
self.client_id = b''
|
||||
self.max_time_dif = 60 * 60 * 24 # time dif (second) setting
|
||||
self.tls_version = b'\x03\x03'
|
||||
self.overhead = 5
|
||||
|
||||
def init_data(self):
|
||||
return obfs_auth_data()
|
||||
|
||||
def get_overhead(self, direction): # direction: true for c->s false for s->c
|
||||
return self.overhead
|
||||
|
||||
def sni(self, url):
|
||||
url = common.to_bytes(url)
|
||||
data = b"\x00" + struct.pack('>H', len(url)) + url
|
||||
data = b"\x00\x00" + struct.pack('>H', len(data) + 2) + struct.pack('>H', len(data)) + data
|
||||
return data
|
||||
|
||||
def pack_auth_data(self, client_id):
|
||||
utc_time = int(time.time()) & 0xFFFFFFFF
|
||||
data = struct.pack('>I', utc_time) + os.urandom(18)
|
||||
data += hmac.new(self.server_info.key + client_id, data, hashlib.sha1).digest()[:10]
|
||||
return data
|
||||
|
||||
def client_encode(self, buf):
|
||||
if self.handshake_status == -1:
|
||||
return buf
|
||||
if self.handshake_status == 8:
|
||||
ret = b''
|
||||
while len(buf) > 2048:
|
||||
size = min(struct.unpack('>H', os.urandom(2))[0] % 4096 + 100, len(buf))
|
||||
ret += b"\x17" + self.tls_version + struct.pack('>H', size) + buf[:size]
|
||||
buf = buf[size:]
|
||||
if len(buf) > 0:
|
||||
ret += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf
|
||||
return ret
|
||||
if len(buf) > 0:
|
||||
self.send_buffer += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf
|
||||
if self.handshake_status == 0:
|
||||
self.handshake_status = 1
|
||||
data = self.tls_version + self.pack_auth_data(self.server_info.data.client_id) + b"\x20" + self.server_info.data.client_id + binascii.unhexlify(b"001cc02bc02fcca9cca8cc14cc13c00ac014c009c013009c0035002f000a" + b"0100")
|
||||
ext = binascii.unhexlify(b"ff01000100")
|
||||
host = self.server_info.obfs_param or self.server_info.host
|
||||
if host and host[-1] in string.digits:
|
||||
host = ''
|
||||
hosts = host.split(',')
|
||||
host = random.choice(hosts)
|
||||
ext += self.sni(host)
|
||||
ext += b"\x00\x17\x00\x00"
|
||||
if host not in self.server_info.data.ticket_buf:
|
||||
self.server_info.data.ticket_buf[host] = os.urandom((struct.unpack('>H', os.urandom(2))[0] % 17 + 8) * 16)
|
||||
ext += b"\x00\x23" + struct.pack('>H', len(self.server_info.data.ticket_buf[host])) + self.server_info.data.ticket_buf[host]
|
||||
ext += binascii.unhexlify(b"000d001600140601060305010503040104030301030302010203")
|
||||
ext += binascii.unhexlify(b"000500050100000000")
|
||||
ext += binascii.unhexlify(b"00120000")
|
||||
ext += binascii.unhexlify(b"75500000")
|
||||
ext += binascii.unhexlify(b"000b00020100")
|
||||
ext += binascii.unhexlify(b"000a0006000400170018")
|
||||
data += struct.pack('>H', len(ext)) + ext
|
||||
data = b"\x01\x00" + struct.pack('>H', len(data)) + data
|
||||
data = b"\x16\x03\x01" + struct.pack('>H', len(data)) + data
|
||||
return data
|
||||
elif self.handshake_status == 1 and len(buf) == 0:
|
||||
data = b"\x14" + self.tls_version + b"\x00\x01\x01" #ChangeCipherSpec
|
||||
data += b"\x16" + self.tls_version + b"\x00\x20" + os.urandom(22) #Finished
|
||||
data += hmac.new(self.server_info.key + self.server_info.data.client_id, data, hashlib.sha1).digest()[:10]
|
||||
ret = data + self.send_buffer
|
||||
self.send_buffer = b''
|
||||
self.handshake_status = 8
|
||||
return ret
|
||||
return b''
|
||||
|
||||
def client_decode(self, buf):
|
||||
if self.handshake_status == -1:
|
||||
return (buf, False)
|
||||
|
||||
if self.handshake_status == 8:
|
||||
ret = b''
|
||||
self.recv_buffer += buf
|
||||
while len(self.recv_buffer) > 5:
|
||||
if ord(self.recv_buffer[0]) != 0x17:
|
||||
logging.info("data = %s" % (binascii.hexlify(self.recv_buffer)))
|
||||
raise Exception('server_decode appdata error')
|
||||
size = struct.unpack('>H', self.recv_buffer[3:5])[0]
|
||||
if len(self.recv_buffer) < size + 5:
|
||||
break
|
||||
buf = self.recv_buffer[5:size+5]
|
||||
ret += buf
|
||||
self.recv_buffer = self.recv_buffer[size+5:]
|
||||
return (ret, False)
|
||||
|
||||
if len(buf) < 11 + 32 + 1 + 32:
|
||||
raise Exception('client_decode data error')
|
||||
verify = buf[11:33]
|
||||
if hmac.new(self.server_info.key + self.server_info.data.client_id, verify, hashlib.sha1).digest()[:10] != buf[33:43]:
|
||||
raise Exception('client_decode data error')
|
||||
if hmac.new(self.server_info.key + self.server_info.data.client_id, buf[:-10], hashlib.sha1).digest()[:10] != buf[-10:]:
|
||||
raise Exception('client_decode data error')
|
||||
return (b'', True)
|
||||
|
||||
def server_encode(self, buf):
|
||||
if self.handshake_status == -1:
|
||||
return buf
|
||||
if (self.handshake_status & 8) == 8:
|
||||
ret = b''
|
||||
while len(buf) > 2048:
|
||||
size = min(struct.unpack('>H', os.urandom(2))[0] % 4096 + 100, len(buf))
|
||||
ret += b"\x17" + self.tls_version + struct.pack('>H', size) + buf[:size]
|
||||
buf = buf[size:]
|
||||
if len(buf) > 0:
|
||||
ret += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf
|
||||
return ret
|
||||
self.handshake_status |= 8
|
||||
data = self.tls_version + self.pack_auth_data(self.client_id) + b"\x20" + self.client_id + binascii.unhexlify(b"c02f000005ff01000100")
|
||||
data = b"\x02\x00" + struct.pack('>H', len(data)) + data #server hello
|
||||
data = b"\x16" + self.tls_version + struct.pack('>H', len(data)) + data
|
||||
if random.randint(0, 8) < 1:
|
||||
ticket = os.urandom((struct.unpack('>H', os.urandom(2))[0] % 164) * 2 + 64)
|
||||
ticket = struct.pack('>H', len(ticket) + 4) + b"\x04\x00" + struct.pack('>H', len(ticket)) + ticket
|
||||
data += b"\x16" + self.tls_version + ticket #New session ticket
|
||||
data += b"\x14" + self.tls_version + b"\x00\x01\x01" #ChangeCipherSpec
|
||||
finish_len = random.choice([32, 40])
|
||||
data += b"\x16" + self.tls_version + struct.pack('>H', finish_len) + os.urandom(finish_len - 10) #Finished
|
||||
data += hmac.new(self.server_info.key + self.client_id, data, hashlib.sha1).digest()[:10]
|
||||
if buf:
|
||||
data += self.server_encode(buf)
|
||||
return data
|
||||
|
||||
def decode_error_return(self, buf):
|
||||
self.handshake_status = -1
|
||||
if self.overhead > 0:
|
||||
self.server_info.overhead -= self.overhead
|
||||
self.overhead = 0
|
||||
if self.method in ['tls1.2_ticket_auth', 'tls1.2_ticket_fastauth']:
|
||||
return (b'E'*2048, False, False)
|
||||
return (buf, True, False)
|
||||
|
||||
def server_decode(self, buf):
|
||||
if self.handshake_status == -1:
|
||||
return (buf, True, False)
|
||||
|
||||
if (self.handshake_status & 4) == 4:
|
||||
ret = b''
|
||||
self.recv_buffer += buf
|
||||
while len(self.recv_buffer) > 5:
|
||||
if ord(self.recv_buffer[0]) != 0x17 or ord(self.recv_buffer[1]) != 0x3 or ord(self.recv_buffer[2]) != 0x3:
|
||||
logging.info("data = %s" % (binascii.hexlify(self.recv_buffer)))
|
||||
raise Exception('server_decode appdata error')
|
||||
size = struct.unpack('>H', self.recv_buffer[3:5])[0]
|
||||
if len(self.recv_buffer) < size + 5:
|
||||
break
|
||||
ret += self.recv_buffer[5:size+5]
|
||||
self.recv_buffer = self.recv_buffer[size+5:]
|
||||
return (ret, True, False)
|
||||
|
||||
if (self.handshake_status & 1) == 1:
|
||||
self.recv_buffer += buf
|
||||
buf = self.recv_buffer
|
||||
verify = buf
|
||||
if len(buf) < 11:
|
||||
raise Exception('server_decode data error')
|
||||
if not match_begin(buf, b"\x14" + self.tls_version + b"\x00\x01\x01"): #ChangeCipherSpec
|
||||
raise Exception('server_decode data error')
|
||||
buf = buf[6:]
|
||||
if not match_begin(buf, b"\x16" + self.tls_version + b"\x00"): #Finished
|
||||
raise Exception('server_decode data error')
|
||||
verify_len = struct.unpack('>H', buf[3:5])[0] + 1 # 11 - 10
|
||||
if len(verify) < verify_len + 10:
|
||||
return (b'', False, False)
|
||||
if hmac.new(self.server_info.key + self.client_id, verify[:verify_len], hashlib.sha1).digest()[:10] != verify[verify_len:verify_len+10]:
|
||||
raise Exception('server_decode data error')
|
||||
self.recv_buffer = verify[verify_len + 10:]
|
||||
status = self.handshake_status
|
||||
self.handshake_status |= 4
|
||||
ret = self.server_decode(b'')
|
||||
return ret;
|
||||
|
||||
#raise Exception("handshake data = %s" % (binascii.hexlify(buf)))
|
||||
self.recv_buffer += buf
|
||||
buf = self.recv_buffer
|
||||
ogn_buf = buf
|
||||
if len(buf) < 3:
|
||||
return (b'', False, False)
|
||||
if not match_begin(buf, b'\x16\x03\x01'):
|
||||
return self.decode_error_return(ogn_buf)
|
||||
buf = buf[3:]
|
||||
header_len = struct.unpack('>H', buf[:2])[0]
|
||||
if header_len > len(buf) - 2:
|
||||
return (b'', False, False)
|
||||
|
||||
self.recv_buffer = self.recv_buffer[header_len + 5:]
|
||||
self.handshake_status = 1
|
||||
buf = buf[2:header_len + 2]
|
||||
if not match_begin(buf, b'\x01\x00'): #client hello
|
||||
logging.info("tls_auth not client hello message")
|
||||
return self.decode_error_return(ogn_buf)
|
||||
buf = buf[2:]
|
||||
if struct.unpack('>H', buf[:2])[0] != len(buf) - 2:
|
||||
logging.info("tls_auth wrong message size")
|
||||
return self.decode_error_return(ogn_buf)
|
||||
buf = buf[2:]
|
||||
if not match_begin(buf, self.tls_version):
|
||||
logging.info("tls_auth wrong tls version")
|
||||
return self.decode_error_return(ogn_buf)
|
||||
buf = buf[2:]
|
||||
verifyid = buf[:32]
|
||||
buf = buf[32:]
|
||||
sessionid_len = ord(buf[0])
|
||||
if sessionid_len < 32:
|
||||
logging.info("tls_auth wrong sessionid_len")
|
||||
return self.decode_error_return(ogn_buf)
|
||||
sessionid = buf[1:sessionid_len + 1]
|
||||
buf = buf[sessionid_len+1:]
|
||||
self.client_id = sessionid
|
||||
sha1 = hmac.new(self.server_info.key + sessionid, verifyid[:22], hashlib.sha1).digest()[:10]
|
||||
utc_time = struct.unpack('>I', verifyid[:4])[0]
|
||||
time_dif = common.int32((int(time.time()) & 0xffffffff) - utc_time)
|
||||
if self.server_info.obfs_param:
|
||||
try:
|
||||
self.max_time_dif = int(self.server_info.obfs_param)
|
||||
except:
|
||||
pass
|
||||
if self.max_time_dif > 0 and (time_dif < -self.max_time_dif or time_dif > self.max_time_dif \
|
||||
or common.int32(utc_time - self.server_info.data.startup_time) < -self.max_time_dif / 2):
|
||||
logging.info("tls_auth wrong time")
|
||||
return self.decode_error_return(ogn_buf)
|
||||
if sha1 != verifyid[22:]:
|
||||
logging.info("tls_auth wrong sha1")
|
||||
return self.decode_error_return(ogn_buf)
|
||||
if self.server_info.data.client_data.get(verifyid[:22]):
|
||||
logging.info("replay attack detect, id = %s" % (binascii.hexlify(verifyid)))
|
||||
return self.decode_error_return(ogn_buf)
|
||||
self.server_info.data.client_data.sweep()
|
||||
self.server_info.data.client_data[verifyid[:22]] = sessionid
|
||||
if len(self.recv_buffer) >= 11:
|
||||
ret = self.server_decode(b'')
|
||||
return (ret[0], True, True)
|
||||
# (buffer_to_recv, is_need_decrypt, is_need_to_encode_and_send_back)
|
||||
return (b'', False, True)
|
||||
|
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2015-2015 breakwa11
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from shadowsocks.common import ord
|
||||
|
||||
def create_obfs(method):
|
||||
return plain(method)
|
||||
|
||||
obfs_map = {
|
||||
'plain': (create_obfs,),
|
||||
'origin': (create_obfs,),
|
||||
}
|
||||
|
||||
class plain(object):
|
||||
def __init__(self, method):
|
||||
self.method = method
|
||||
self.server_info = None
|
||||
|
||||
def init_data(self):
|
||||
return b''
|
||||
|
||||
def get_overhead(self, direction): # direction: true for c->s false for s->c
|
||||
return 0
|
||||
|
||||
def get_server_info(self):
|
||||
return self.server_info
|
||||
|
||||
def set_server_info(self, server_info):
|
||||
self.server_info = server_info
|
||||
|
||||
def client_pre_encrypt(self, buf):
|
||||
return buf
|
||||
|
||||
def client_encode(self, buf):
|
||||
return buf
|
||||
|
||||
def client_decode(self, buf):
|
||||
# (buffer_to_recv, is_need_to_encode_and_send_back)
|
||||
return (buf, False)
|
||||
|
||||
def client_post_decrypt(self, buf):
|
||||
return buf
|
||||
|
||||
def server_pre_encrypt(self, buf):
|
||||
return buf
|
||||
|
||||
def server_encode(self, buf):
|
||||
return buf
|
||||
|
||||
def server_decode(self, buf):
|
||||
# (buffer_to_recv, is_need_decrypt, is_need_to_encode_and_send_back)
|
||||
return (buf, True, False)
|
||||
|
||||
def server_post_decrypt(self, buf):
|
||||
return (buf, False)
|
||||
|
||||
def client_udp_pre_encrypt(self, buf):
|
||||
return buf
|
||||
|
||||
def client_udp_post_decrypt(self, buf):
|
||||
return buf
|
||||
|
||||
def server_udp_pre_encrypt(self, buf, uid):
|
||||
return buf
|
||||
|
||||
def server_udp_post_decrypt(self, buf):
|
||||
return (buf, None)
|
||||
|
||||
def dispose(self):
|
||||
pass
|
||||
|
||||
def get_head_size(self, buf, def_value):
|
||||
if len(buf) < 2:
|
||||
return def_value
|
||||
head_type = ord(buf[0]) & 0x7
|
||||
if head_type == 1:
|
||||
return 7
|
||||
if head_type == 4:
|
||||
return 19
|
||||
if head_type == 3:
|
||||
return 4 + ord(buf[1])
|
||||
return def_value
|
||||
|
@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2015-2015 breakwa11
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
import binascii
|
||||
import base64
|
||||
import time
|
||||
import datetime
|
||||
import random
|
||||
import struct
|
||||
import zlib
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
import shadowsocks
|
||||
from shadowsocks import common
|
||||
from shadowsocks.obfsplugin import plain
|
||||
from shadowsocks.common import to_bytes, to_str, ord, chr
|
||||
|
||||
def create_verify_deflate(method):
|
||||
return verify_deflate(method)
|
||||
|
||||
obfs_map = {
|
||||
'verify_deflate': (create_verify_deflate,),
|
||||
}
|
||||
|
||||
def match_begin(str1, str2):
|
||||
if len(str1) >= len(str2):
|
||||
if str1[:len(str2)] == str2:
|
||||
return True
|
||||
return False
|
||||
|
||||
class obfs_verify_data(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
class verify_base(plain.plain):
|
||||
def __init__(self, method):
|
||||
super(verify_base, self).__init__(method)
|
||||
self.method = method
|
||||
|
||||
def init_data(self):
|
||||
return obfs_verify_data()
|
||||
|
||||
def set_server_info(self, server_info):
|
||||
self.server_info = server_info
|
||||
|
||||
def client_encode(self, buf):
|
||||
return buf
|
||||
|
||||
def client_decode(self, buf):
|
||||
return (buf, False)
|
||||
|
||||
def server_encode(self, buf):
|
||||
return buf
|
||||
|
||||
def server_decode(self, buf):
|
||||
return (buf, True, False)
|
||||
|
||||
class verify_deflate(verify_base):
|
||||
def __init__(self, method):
|
||||
super(verify_deflate, self).__init__(method)
|
||||
self.recv_buf = b''
|
||||
self.unit_len = 32700
|
||||
self.decrypt_packet_num = 0
|
||||
self.raw_trans = False
|
||||
|
||||
def pack_data(self, buf):
|
||||
if len(buf) == 0:
|
||||
return b''
|
||||
data = zlib.compress(buf)
|
||||
data = struct.pack('>H', len(data)) + data[2:]
|
||||
return data
|
||||
|
||||
def client_pre_encrypt(self, buf):
|
||||
ret = b''
|
||||
while len(buf) > self.unit_len:
|
||||
ret += self.pack_data(buf[:self.unit_len])
|
||||
buf = buf[self.unit_len:]
|
||||
ret += self.pack_data(buf)
|
||||
return ret
|
||||
|
||||
def client_post_decrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return buf
|
||||
self.recv_buf += buf
|
||||
out_buf = b''
|
||||
while len(self.recv_buf) > 2:
|
||||
length = struct.unpack('>H', self.recv_buf[:2])[0]
|
||||
if length >= 32768 or length < 6:
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
raise Exception('client_post_decrypt data error')
|
||||
if length > len(self.recv_buf):
|
||||
break
|
||||
|
||||
out_buf += zlib.decompress(b'x\x9c' + self.recv_buf[2:length])
|
||||
self.recv_buf = self.recv_buf[length:]
|
||||
|
||||
if out_buf:
|
||||
self.decrypt_packet_num += 1
|
||||
return out_buf
|
||||
|
||||
def server_pre_encrypt(self, buf):
|
||||
ret = b''
|
||||
while len(buf) > self.unit_len:
|
||||
ret += self.pack_data(buf[:self.unit_len])
|
||||
buf = buf[self.unit_len:]
|
||||
ret += self.pack_data(buf)
|
||||
return ret
|
||||
|
||||
def server_post_decrypt(self, buf):
|
||||
if self.raw_trans:
|
||||
return (buf, False)
|
||||
self.recv_buf += buf
|
||||
out_buf = b''
|
||||
while len(self.recv_buf) > 2:
|
||||
length = struct.unpack('>H', self.recv_buf[:2])[0]
|
||||
if length >= 32768 or length < 6:
|
||||
self.raw_trans = True
|
||||
self.recv_buf = b''
|
||||
if self.decrypt_packet_num == 0:
|
||||
return (b'E'*2048, False)
|
||||
else:
|
||||
raise Exception('server_post_decrype data error')
|
||||
if length > len(self.recv_buf):
|
||||
break
|
||||
|
||||
out_buf += zlib.decompress(b'\x78\x9c' + self.recv_buf[2:length])
|
||||
self.recv_buf = self.recv_buf[length:]
|
||||
|
||||
if out_buf:
|
||||
self.decrypt_packet_num += 1
|
||||
return (out_buf, False)
|
||||
|
@ -0,0 +1,214 @@
|
||||
import collections
|
||||
|
||||
################################################################################
|
||||
### OrderedDict
|
||||
################################################################################
|
||||
|
||||
class OrderedDict(dict):
|
||||
'Dictionary that remembers insertion order'
|
||||
# An inherited dict maps keys to values.
|
||||
# The inherited dict provides __getitem__, __len__, __contains__, and get.
|
||||
# The remaining methods are order-aware.
|
||||
# Big-O running times for all methods are the same as regular dictionaries.
|
||||
|
||||
# The internal self.__map dict maps keys to links in a doubly linked list.
|
||||
# The circular doubly linked list starts and ends with a sentinel element.
|
||||
# The sentinel element never gets deleted (this simplifies the algorithm).
|
||||
# Each link is stored as a list of length three: [PREV, NEXT, KEY].
|
||||
|
||||
def __init__(*args, **kwds):
|
||||
'''Initialize an ordered dictionary. The signature is the same as
|
||||
regular dictionaries, but keyword arguments are not recommended because
|
||||
their insertion order is arbitrary.
|
||||
|
||||
'''
|
||||
if not args:
|
||||
raise TypeError("descriptor '__init__' of 'OrderedDict' object "
|
||||
"needs an argument")
|
||||
self = args[0]
|
||||
args = args[1:]
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||
try:
|
||||
self.__root
|
||||
except AttributeError:
|
||||
self.__root = root = [] # sentinel node
|
||||
root[:] = [root, root, None]
|
||||
self.__map = {}
|
||||
self.__update(*args, **kwds)
|
||||
|
||||
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
|
||||
'od.__setitem__(i, y) <==> od[i]=y'
|
||||
# Setting a new item creates a new link at the end of the linked list,
|
||||
# and the inherited dictionary is updated with the new key/value pair.
|
||||
if key not in self:
|
||||
root = self.__root
|
||||
last = root[0]
|
||||
last[1] = root[0] = self.__map[key] = [last, root, key]
|
||||
return dict_setitem(self, key, value)
|
||||
|
||||
def __delitem__(self, key, dict_delitem=dict.__delitem__):
|
||||
'od.__delitem__(y) <==> del od[y]'
|
||||
# Deleting an existing item uses self.__map to find the link which gets
|
||||
# removed by updating the links in the predecessor and successor nodes.
|
||||
dict_delitem(self, key)
|
||||
link_prev, link_next, _ = self.__map.pop(key)
|
||||
link_prev[1] = link_next # update link_prev[NEXT]
|
||||
link_next[0] = link_prev # update link_next[PREV]
|
||||
|
||||
def __iter__(self):
|
||||
'od.__iter__() <==> iter(od)'
|
||||
# Traverse the linked list in order.
|
||||
root = self.__root
|
||||
curr = root[1] # start at the first node
|
||||
while curr is not root:
|
||||
yield curr[2] # yield the curr[KEY]
|
||||
curr = curr[1] # move to next node
|
||||
|
||||
def __reversed__(self):
|
||||
'od.__reversed__() <==> reversed(od)'
|
||||
# Traverse the linked list in reverse order.
|
||||
root = self.__root
|
||||
curr = root[0] # start at the last node
|
||||
while curr is not root:
|
||||
yield curr[2] # yield the curr[KEY]
|
||||
curr = curr[0] # move to previous node
|
||||
|
||||
def clear(self):
|
||||
'od.clear() -> None. Remove all items from od.'
|
||||
root = self.__root
|
||||
root[:] = [root, root, None]
|
||||
self.__map.clear()
|
||||
dict.clear(self)
|
||||
|
||||
# -- the following methods do not depend on the internal structure --
|
||||
|
||||
def keys(self):
|
||||
'od.keys() -> list of keys in od'
|
||||
return list(self)
|
||||
|
||||
def values(self):
|
||||
'od.values() -> list of values in od'
|
||||
return [self[key] for key in self]
|
||||
|
||||
def items(self):
|
||||
'od.items() -> list of (key, value) pairs in od'
|
||||
return [(key, self[key]) for key in self]
|
||||
|
||||
def iterkeys(self):
|
||||
'od.iterkeys() -> an iterator over the keys in od'
|
||||
return iter(self)
|
||||
|
||||
def itervalues(self):
|
||||
'od.itervalues -> an iterator over the values in od'
|
||||
for k in self:
|
||||
yield self[k]
|
||||
|
||||
def iteritems(self):
|
||||
'od.iteritems -> an iterator over the (key, value) pairs in od'
|
||||
for k in self:
|
||||
yield (k, self[k])
|
||||
|
||||
update = collections.MutableMapping.update
|
||||
|
||||
__update = update # let subclasses override update without breaking __init__
|
||||
|
||||
__marker = object()
|
||||
|
||||
def pop(self, key, default=__marker):
|
||||
'''od.pop(k[,d]) -> v, remove specified key and return the corresponding
|
||||
value. If key is not found, d is returned if given, otherwise KeyError
|
||||
is raised.
|
||||
|
||||
'''
|
||||
if key in self:
|
||||
result = self[key]
|
||||
del self[key]
|
||||
return result
|
||||
if default is self.__marker:
|
||||
raise KeyError(key)
|
||||
return default
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
|
||||
if key in self:
|
||||
return self[key]
|
||||
self[key] = default
|
||||
return default
|
||||
|
||||
def popitem(self, last=True):
|
||||
'''od.popitem() -> (k, v), return and remove a (key, value) pair.
|
||||
Pairs are returned in LIFO order if last is true or FIFO order if false.
|
||||
|
||||
'''
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
key = next(reversed(self) if last else iter(self))
|
||||
value = self.pop(key)
|
||||
return key, value
|
||||
|
||||
def __repr__(self, _repr_running={}):
|
||||
'od.__repr__() <==> repr(od)'
|
||||
call_key = id(self), _get_ident()
|
||||
if call_key in _repr_running:
|
||||
return '...'
|
||||
_repr_running[call_key] = 1
|
||||
try:
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__,)
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
finally:
|
||||
del _repr_running[call_key]
|
||||
|
||||
def __reduce__(self):
|
||||
'Return state information for pickling'
|
||||
items = [[k, self[k]] for k in self]
|
||||
inst_dict = vars(self).copy()
|
||||
for k in vars(OrderedDict()):
|
||||
inst_dict.pop(k, None)
|
||||
if inst_dict:
|
||||
return (self.__class__, (items,), inst_dict)
|
||||
return self.__class__, (items,)
|
||||
|
||||
def copy(self):
|
||||
'od.copy() -> a shallow copy of od'
|
||||
return self.__class__(self)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
'''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S.
|
||||
If not specified, the value defaults to None.
|
||||
|
||||
'''
|
||||
self = cls()
|
||||
for key in iterable:
|
||||
self[key] = value
|
||||
return self
|
||||
|
||||
def __eq__(self, other):
|
||||
'''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
|
||||
while comparison to a regular mapping is order-insensitive.
|
||||
|
||||
'''
|
||||
if isinstance(other, OrderedDict):
|
||||
return dict.__eq__(self, other) and all(_imap(_eq, self, other))
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
'od.__ne__(y) <==> od!=y'
|
||||
return not self == other
|
||||
|
||||
# -- the following methods support python 3.x style dictionary views --
|
||||
|
||||
def viewkeys(self):
|
||||
"od.viewkeys() -> a set-like object providing a view on od's keys"
|
||||
return KeysView(self)
|
||||
|
||||
def viewvalues(self):
|
||||
"od.viewvalues() -> an object providing a view on od's values"
|
||||
return ValuesView(self)
|
||||
|
||||
def viewitems(self):
|
||||
"od.viewitems() -> a set-like object providing a view on od's items"
|
||||
return ItemsView(self)
|
||||
|
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd `dirname $0`
|
||||
eval $(ps -ef | grep "[0-9] python3 server\\.py a" | awk '{print "kill "$2}')
|
||||
ulimit -n 512000
|
||||
nohup python3 server.py a >> /dev/null 2>&1 &
|
@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 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
|
||||
|
||||
if __name__ == '__main__':
|
||||
import inspect
|
||||
file_path = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe())))
|
||||
sys.path.insert(0, os.path.join(file_path, '../'))
|
||||
|
||||
from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, \
|
||||
asyncdns, manager, common
|
||||
|
||||
|
||||
def main():
|
||||
shell.check_python3()
|
||||
|
||||
config = shell.get_config(False)
|
||||
|
||||
shell.log_shadowsocks_version()
|
||||
|
||||
daemon.daemon_exec(config)
|
||||
|
||||
try:
|
||||
import resource
|
||||
logging.info('current process RLIMIT_NOFILE resource: soft %d hard %d' % resource.getrlimit(resource.RLIMIT_NOFILE))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if config['port_password']:
|
||||
pass
|
||||
else:
|
||||
config['port_password'] = {}
|
||||
server_port = config['server_port']
|
||||
if type(server_port) == list:
|
||||
for a_server_port in server_port:
|
||||
config['port_password'][a_server_port] = config['password']
|
||||
else:
|
||||
config['port_password'][str(server_port)] = config['password']
|
||||
|
||||
if not config.get('dns_ipv6', False):
|
||||
asyncdns.IPV6_CONNECTION_SUPPORT = False
|
||||
|
||||
if config.get('manager_address', 0):
|
||||
logging.info('entering manager mode')
|
||||
manager.run(config)
|
||||
return
|
||||
|
||||
tcp_servers = []
|
||||
udp_servers = []
|
||||
dns_resolver = asyncdns.DNSResolver()
|
||||
if int(config['workers']) > 1:
|
||||
stat_counter_dict = None
|
||||
else:
|
||||
stat_counter_dict = {}
|
||||
port_password = config['port_password']
|
||||
config_password = config.get('password', 'm')
|
||||
del config['port_password']
|
||||
for port, password_obfs in port_password.items():
|
||||
method = config["method"]
|
||||
protocol = config.get("protocol", 'origin')
|
||||
protocol_param = config.get("protocol_param", '')
|
||||
obfs = config.get("obfs", 'plain')
|
||||
obfs_param = config.get("obfs_param", '')
|
||||
bind = config.get("out_bind", '')
|
||||
bindv6 = config.get("out_bindv6", '')
|
||||
if type(password_obfs) == list:
|
||||
password = password_obfs[0]
|
||||
obfs = common.to_str(password_obfs[1])
|
||||
if len(password_obfs) > 2:
|
||||
protocol = common.to_str(password_obfs[2])
|
||||
elif type(password_obfs) == dict:
|
||||
password = password_obfs.get('password', config_password)
|
||||
method = common.to_str(password_obfs.get('method', method))
|
||||
protocol = common.to_str(password_obfs.get('protocol', protocol))
|
||||
protocol_param = common.to_str(password_obfs.get('protocol_param', protocol_param))
|
||||
obfs = common.to_str(password_obfs.get('obfs', obfs))
|
||||
obfs_param = common.to_str(password_obfs.get('obfs_param', obfs_param))
|
||||
bind = password_obfs.get('out_bind', bind)
|
||||
bindv6 = password_obfs.get('out_bindv6', bindv6)
|
||||
else:
|
||||
password = password_obfs
|
||||
a_config = config.copy()
|
||||
ipv6_ok = False
|
||||
logging.info("server start with protocol[%s] password [%s] method [%s] obfs [%s] obfs_param [%s]" %
|
||||
(protocol, password, method, obfs, obfs_param))
|
||||
if 'server_ipv6' in a_config:
|
||||
try:
|
||||
if len(a_config['server_ipv6']) > 2 and a_config['server_ipv6'][0] == "[" and a_config['server_ipv6'][-1] == "]":
|
||||
a_config['server_ipv6'] = a_config['server_ipv6'][1:-1]
|
||||
a_config['server_port'] = int(port)
|
||||
a_config['password'] = password
|
||||
a_config['method'] = method
|
||||
a_config['protocol'] = protocol
|
||||
a_config['protocol_param'] = protocol_param
|
||||
a_config['obfs'] = obfs
|
||||
a_config['obfs_param'] = obfs_param
|
||||
a_config['out_bind'] = bind
|
||||
a_config['out_bindv6'] = bindv6
|
||||
a_config['server'] = a_config['server_ipv6']
|
||||
logging.info("starting server at [%s]:%d" %
|
||||
(a_config['server'], int(port)))
|
||||
tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict))
|
||||
udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict))
|
||||
if a_config['server_ipv6'] == b"::":
|
||||
ipv6_ok = True
|
||||
except Exception as e:
|
||||
shell.print_exception(e)
|
||||
|
||||
try:
|
||||
a_config = config.copy()
|
||||
a_config['server_port'] = int(port)
|
||||
a_config['password'] = password
|
||||
a_config['method'] = method
|
||||
a_config['protocol'] = protocol
|
||||
a_config['protocol_param'] = protocol_param
|
||||
a_config['obfs'] = obfs
|
||||
a_config['obfs_param'] = obfs_param
|
||||
a_config['out_bind'] = bind
|
||||
a_config['out_bindv6'] = bindv6
|
||||
logging.info("starting server at %s:%d" %
|
||||
(a_config['server'], int(port)))
|
||||
tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict))
|
||||
udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict))
|
||||
except Exception as e:
|
||||
if not ipv6_ok:
|
||||
shell.print_exception(e)
|
||||
|
||||
def run_server():
|
||||
def child_handler(signum, _):
|
||||
logging.warn('received SIGQUIT, doing graceful shutting down..')
|
||||
list(map(lambda s: s.close(next_tick=True),
|
||||
tcp_servers + udp_servers))
|
||||
signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM),
|
||||
child_handler)
|
||||
|
||||
def int_handler(signum, _):
|
||||
sys.exit(1)
|
||||
signal.signal(signal.SIGINT, int_handler)
|
||||
|
||||
try:
|
||||
loop = eventloop.EventLoop()
|
||||
dns_resolver.add_to_loop(loop)
|
||||
list(map(lambda s: s.add_to_loop(loop), tcp_servers + udp_servers))
|
||||
|
||||
daemon.set_user(config.get('user', None))
|
||||
loop.run()
|
||||
except Exception as e:
|
||||
shell.print_exception(e)
|
||||
sys.exit(1)
|
||||
|
||||
if int(config['workers']) > 1:
|
||||
if os.name == 'posix':
|
||||
children = []
|
||||
is_child = False
|
||||
for i in range(0, int(config['workers'])):
|
||||
r = os.fork()
|
||||
if r == 0:
|
||||
logging.info('worker started')
|
||||
is_child = True
|
||||
run_server()
|
||||
break
|
||||
else:
|
||||
children.append(r)
|
||||
if not is_child:
|
||||
def handler(signum, _):
|
||||
for pid in children:
|
||||
try:
|
||||
os.kill(pid, signum)
|
||||
os.waitpid(pid, 0)
|
||||
except OSError: # child may already exited
|
||||
pass
|
||||
sys.exit()
|
||||
signal.signal(signal.SIGTERM, handler)
|
||||
signal.signal(signal.SIGQUIT, handler)
|
||||
signal.signal(signal.SIGINT, handler)
|
||||
|
||||
# master
|
||||
for a_tcp_server in tcp_servers:
|
||||
a_tcp_server.close()
|
||||
for a_udp_server in udp_servers:
|
||||
a_udp_server.close()
|
||||
dns_resolver.close()
|
||||
|
||||
for child in children:
|
||||
os.waitpid(child, 0)
|
||||
else:
|
||||
logging.warn('worker is only available on Unix/Linux')
|
||||
run_server()
|
||||
else:
|
||||
run_server()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,445 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 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 os
|
||||
import json
|
||||
import sys
|
||||
import getopt
|
||||
import logging
|
||||
from shadowsocks.common import to_bytes, to_str, IPNetwork, PortRange
|
||||
from shadowsocks import encrypt
|
||||
|
||||
|
||||
VERBOSE_LEVEL = 5
|
||||
|
||||
verbose = 0
|
||||
|
||||
|
||||
def check_python3():
|
||||
info = sys.version_info
|
||||
if info[0] == 2 and not info[1] >= 6:
|
||||
print('Python 2.6+ required')
|
||||
sys.exit(1)
|
||||
elif info[0] == 3 and not info[1] >= 3:
|
||||
print('Python 3.3+ required')
|
||||
sys.exit(1)
|
||||
elif info[0] not in [2, 3]:
|
||||
print('Python version not supported')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def print_exception(e):
|
||||
global verbose
|
||||
logging.error(e)
|
||||
if verbose > 0:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def __version():
|
||||
version_str = ''
|
||||
try:
|
||||
import pkg_resources
|
||||
version_str = pkg_resources.get_distribution('shadowsocks').version
|
||||
except Exception:
|
||||
try:
|
||||
from shadowsocks import version
|
||||
version_str = version.version()
|
||||
except Exception:
|
||||
pass
|
||||
return version_str
|
||||
|
||||
def print_shadowsocks():
|
||||
print('ShadowsocksR %s' % __version())
|
||||
|
||||
def log_shadowsocks_version():
|
||||
logging.info('ShadowsocksR %s' % __version())
|
||||
|
||||
|
||||
def find_config():
|
||||
user_config_path = 'user-config.json'
|
||||
config_path = 'config.json'
|
||||
|
||||
def sub_find(file_name):
|
||||
if os.path.exists(file_name):
|
||||
return file_name
|
||||
file_name = os.path.join(os.path.abspath('..'), file_name)
|
||||
return file_name if os.path.exists(file_name) else None
|
||||
|
||||
return sub_find(user_config_path) or sub_find(config_path)
|
||||
|
||||
def check_config(config, is_local):
|
||||
if config.get('daemon', None) == 'stop':
|
||||
# no need to specify configuration for daemon stop
|
||||
return
|
||||
|
||||
if is_local and not config.get('password', None):
|
||||
logging.error('password not specified')
|
||||
print_help(is_local)
|
||||
sys.exit(2)
|
||||
|
||||
if not is_local and not config.get('password', None) \
|
||||
and not config.get('port_password', None):
|
||||
logging.error('password or port_password not specified')
|
||||
print_help(is_local)
|
||||
sys.exit(2)
|
||||
|
||||
if 'local_port' in config:
|
||||
config['local_port'] = int(config['local_port'])
|
||||
|
||||
if 'server_port' in config and type(config['server_port']) != list:
|
||||
config['server_port'] = int(config['server_port'])
|
||||
|
||||
if config.get('local_address', '') in [b'0.0.0.0']:
|
||||
logging.warning('warning: local set to listen on 0.0.0.0, it\'s not safe')
|
||||
if config.get('server', '') in ['127.0.0.1', 'localhost']:
|
||||
logging.warning('warning: server set to listen on %s:%s, are you sure?' %
|
||||
(to_str(config['server']), config['server_port']))
|
||||
if config.get('timeout', 300) < 100:
|
||||
logging.warning('warning: your timeout %d seems too short' %
|
||||
int(config.get('timeout')))
|
||||
if config.get('timeout', 300) > 600:
|
||||
logging.warning('warning: your timeout %d seems too long' %
|
||||
int(config.get('timeout')))
|
||||
if config.get('password') in [b'mypassword']:
|
||||
logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your '
|
||||
'config.json!')
|
||||
sys.exit(1)
|
||||
if config.get('user', None) is not None:
|
||||
if os.name != 'posix':
|
||||
logging.error('user can be used only on Unix')
|
||||
sys.exit(1)
|
||||
|
||||
encrypt.try_cipher(config['password'], config['method'])
|
||||
|
||||
|
||||
def get_config(is_local):
|
||||
global verbose
|
||||
config = {}
|
||||
config_path = None
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(levelname)-s: %(message)s')
|
||||
if is_local:
|
||||
shortopts = 'hd:s:b:p:k:l:m:O:o:G:g:c:t:vq'
|
||||
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=',
|
||||
'version']
|
||||
else:
|
||||
shortopts = 'hd:s:p:k:m:O:o:G:g:c:t:vq'
|
||||
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=',
|
||||
'forbidden-ip=', 'user=', 'manager-address=', 'version']
|
||||
try:
|
||||
optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
|
||||
for key, value in optlist:
|
||||
if key == '-c':
|
||||
config_path = value
|
||||
elif key in ('-h', '--help'):
|
||||
print_help(is_local)
|
||||
sys.exit(0)
|
||||
elif key == '--version':
|
||||
print_shadowsocks()
|
||||
sys.exit(0)
|
||||
else:
|
||||
continue
|
||||
|
||||
if config_path is None:
|
||||
config_path = find_config()
|
||||
|
||||
|
||||
if config_path:
|
||||
logging.debug('loading config from %s' % config_path)
|
||||
with open(config_path, 'rb') as f:
|
||||
try:
|
||||
config = parse_json_in_str(remove_comment(f.read().decode('utf8')))
|
||||
except ValueError as e:
|
||||
logging.error('found an error in config.json: %s', str(e))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
v_count = 0
|
||||
for key, value in optlist:
|
||||
if key == '-p':
|
||||
config['server_port'] = int(value)
|
||||
elif key == '-k':
|
||||
config['password'] = to_bytes(value)
|
||||
elif key == '-l':
|
||||
config['local_port'] = int(value)
|
||||
elif key == '-s':
|
||||
config['server'] = to_str(value)
|
||||
elif key == '-m':
|
||||
config['method'] = to_str(value)
|
||||
elif key == '-O':
|
||||
config['protocol'] = to_str(value)
|
||||
elif key == '-o':
|
||||
config['obfs'] = to_str(value)
|
||||
elif key == '-G':
|
||||
config['protocol_param'] = to_str(value)
|
||||
elif key == '-g':
|
||||
config['obfs_param'] = to_str(value)
|
||||
elif key == '-b':
|
||||
config['local_address'] = to_str(value)
|
||||
elif key == '-v':
|
||||
v_count += 1
|
||||
# '-vv' turns on more verbose mode
|
||||
config['verbose'] = v_count
|
||||
elif key == '-t':
|
||||
config['timeout'] = int(value)
|
||||
elif key == '--fast-open':
|
||||
config['fast_open'] = True
|
||||
elif key == '--workers':
|
||||
config['workers'] = int(value)
|
||||
elif key == '--manager-address':
|
||||
config['manager_address'] = value
|
||||
elif key == '--user':
|
||||
config['user'] = to_str(value)
|
||||
elif key == '--forbidden-ip':
|
||||
config['forbidden_ip'] = to_str(value)
|
||||
|
||||
elif key == '-d':
|
||||
config['daemon'] = to_str(value)
|
||||
elif key == '--pid-file':
|
||||
config['pid-file'] = to_str(value)
|
||||
elif key == '--log-file':
|
||||
config['log-file'] = to_str(value)
|
||||
elif key == '-q':
|
||||
v_count -= 1
|
||||
config['verbose'] = v_count
|
||||
else:
|
||||
continue
|
||||
except getopt.GetoptError as e:
|
||||
print(e, file=sys.stderr)
|
||||
print_help(is_local)
|
||||
sys.exit(2)
|
||||
|
||||
if not config:
|
||||
logging.error('config not specified')
|
||||
print_help(is_local)
|
||||
sys.exit(2)
|
||||
|
||||
config['password'] = to_bytes(config.get('password', b''))
|
||||
config['method'] = to_str(config.get('method', 'aes-256-cfb'))
|
||||
config['protocol'] = to_str(config.get('protocol', 'origin'))
|
||||
config['protocol_param'] = to_str(config.get('protocol_param', ''))
|
||||
config['obfs'] = to_str(config.get('obfs', 'plain'))
|
||||
config['obfs_param'] = to_str(config.get('obfs_param', ''))
|
||||
config['port_password'] = config.get('port_password', None)
|
||||
config['additional_ports'] = config.get('additional_ports', {})
|
||||
config['additional_ports_only'] = config.get('additional_ports_only', False)
|
||||
config['timeout'] = int(config.get('timeout', 300))
|
||||
config['udp_timeout'] = int(config.get('udp_timeout', 120))
|
||||
config['udp_cache'] = int(config.get('udp_cache', 64))
|
||||
config['fast_open'] = config.get('fast_open', False)
|
||||
config['workers'] = config.get('workers', 1)
|
||||
config['pid-file'] = config.get('pid-file', '/var/run/shadowsocksr.pid')
|
||||
config['log-file'] = config.get('log-file', '/var/log/shadowsocksr.log')
|
||||
config['verbose'] = config.get('verbose', False)
|
||||
config['connect_verbose_info'] = config.get('connect_verbose_info', 0)
|
||||
config['local_address'] = to_str(config.get('local_address', '127.0.0.1'))
|
||||
config['local_port'] = config.get('local_port', 1080)
|
||||
if is_local:
|
||||
if config.get('server', None) is None:
|
||||
logging.error('server addr not specified')
|
||||
print_local_help()
|
||||
sys.exit(2)
|
||||
else:
|
||||
config['server'] = to_str(config['server'])
|
||||
else:
|
||||
config['server'] = to_str(config.get('server', '0.0.0.0'))
|
||||
try:
|
||||
config['forbidden_ip'] = \
|
||||
IPNetwork(config.get('forbidden_ip', '127.0.0.0/8,::1/128'))
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
sys.exit(2)
|
||||
try:
|
||||
config['forbidden_port'] = PortRange(config.get('forbidden_port', ''))
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
sys.exit(2)
|
||||
try:
|
||||
config['ignore_bind'] = \
|
||||
IPNetwork(config.get('ignore_bind', '127.0.0.0/8,::1/128,10.0.0.0/8,192.168.0.0/16'))
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
sys.exit(2)
|
||||
config['server_port'] = config.get('server_port', 8388)
|
||||
|
||||
logging.getLogger('').handlers = []
|
||||
logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE')
|
||||
if config['verbose'] >= 2:
|
||||
level = VERBOSE_LEVEL
|
||||
elif config['verbose'] == 1:
|
||||
level = logging.DEBUG
|
||||
elif config['verbose'] == -1:
|
||||
level = logging.WARN
|
||||
elif config['verbose'] <= -2:
|
||||
level = logging.ERROR
|
||||
else:
|
||||
level = logging.INFO
|
||||
verbose = config['verbose']
|
||||
logging.basicConfig(level=level,
|
||||
format='%(asctime)s %(levelname)-8s %(filename)s:%(lineno)s %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S')
|
||||
|
||||
check_config(config, is_local)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def print_help(is_local):
|
||||
if is_local:
|
||||
print_local_help()
|
||||
else:
|
||||
print_server_help()
|
||||
|
||||
|
||||
def print_local_help():
|
||||
print('''usage: sslocal [OPTION]...
|
||||
A fast tunnel proxy that helps you bypass firewalls.
|
||||
|
||||
You can supply configurations via either config file or command line arguments.
|
||||
|
||||
Proxy options:
|
||||
-c CONFIG path to config file
|
||||
-s SERVER_ADDR server address
|
||||
-p SERVER_PORT server port, default: 8388
|
||||
-b LOCAL_ADDR local binding address, default: 127.0.0.1
|
||||
-l LOCAL_PORT local port, default: 1080
|
||||
-k PASSWORD password
|
||||
-m METHOD encryption method, default: aes-256-cfb
|
||||
-o OBFS obfsplugin, default: http_simple
|
||||
-t TIMEOUT timeout in seconds, default: 300
|
||||
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
|
||||
|
||||
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
|
||||
-v, -vv verbose mode
|
||||
-q, -qq quiet mode, only show warnings/errors
|
||||
--version show version information
|
||||
|
||||
Online help: <https://github.com/shadowsocks/shadowsocks>
|
||||
''')
|
||||
|
||||
|
||||
def print_server_help():
|
||||
print('''usage: ssserver [OPTION]...
|
||||
A fast tunnel proxy that helps you bypass firewalls.
|
||||
|
||||
You can supply configurations via either config file or command line arguments.
|
||||
|
||||
Proxy options:
|
||||
-c CONFIG path to config file
|
||||
-s SERVER_ADDR server address, default: 0.0.0.0
|
||||
-p SERVER_PORT server port, default: 8388
|
||||
-k PASSWORD password
|
||||
-m METHOD encryption method, default: aes-256-cfb
|
||||
-o OBFS obfsplugin, default: http_simple
|
||||
-t TIMEOUT timeout in seconds, default: 300
|
||||
--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
|
||||
|
||||
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
|
||||
-v, -vv verbose mode
|
||||
-q, -qq quiet mode, only show warnings/errors
|
||||
--version show version information
|
||||
|
||||
Online help: <https://github.com/shadowsocks/shadowsocks>
|
||||
''')
|
||||
|
||||
|
||||
def _decode_list(data):
|
||||
rv = []
|
||||
for item in data:
|
||||
if hasattr(item, 'encode'):
|
||||
item = item.encode('utf-8')
|
||||
elif isinstance(item, list):
|
||||
item = _decode_list(item)
|
||||
elif isinstance(item, dict):
|
||||
item = _decode_dict(item)
|
||||
rv.append(item)
|
||||
return rv
|
||||
|
||||
|
||||
def _decode_dict(data):
|
||||
rv = {}
|
||||
for key, value in data.items():
|
||||
if hasattr(value, 'encode'):
|
||||
value = value.encode('utf-8')
|
||||
elif isinstance(value, list):
|
||||
value = _decode_list(value)
|
||||
elif isinstance(value, dict):
|
||||
value = _decode_dict(value)
|
||||
rv[key] = value
|
||||
return rv
|
||||
|
||||
class JSFormat:
|
||||
def __init__(self):
|
||||
self.state = 0
|
||||
|
||||
def push(self, ch):
|
||||
ch = ord(ch)
|
||||
if self.state == 0:
|
||||
if ch == ord('"'):
|
||||
self.state = 1
|
||||
return to_str(chr(ch))
|
||||
elif ch == ord('/'):
|
||||
self.state = 3
|
||||
else:
|
||||
return to_str(chr(ch))
|
||||
elif self.state == 1:
|
||||
if ch == ord('"'):
|
||||
self.state = 0
|
||||
return to_str(chr(ch))
|
||||
elif ch == ord('\\'):
|
||||
self.state = 2
|
||||
return to_str(chr(ch))
|
||||
elif self.state == 2:
|
||||
self.state = 1
|
||||
if ch == ord('"'):
|
||||
return to_str(chr(ch))
|
||||
return "\\" + to_str(chr(ch))
|
||||
elif self.state == 3:
|
||||
if ch == ord('/'):
|
||||
self.state = 4
|
||||
else:
|
||||
return "/" + to_str(chr(ch))
|
||||
elif self.state == 4:
|
||||
if ch == ord('\n'):
|
||||
self.state = 0
|
||||
return "\n"
|
||||
return ""
|
||||
|
||||
def remove_comment(json):
|
||||
fmt = JSFormat()
|
||||
return "".join([fmt.push(c) for c in json])
|
||||
|
||||
|
||||
def parse_json_in_str(data):
|
||||
# parse json and convert everything from unicode to str
|
||||
return json.loads(data, object_hook=_decode_dict)
|
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
#python3_ver=$(ls /usr/bin|grep -e "^python3[23]\.[1-9]\+$"|tail -1)
|
||||
eval $(ps -ef | grep "[0-9] python3 server\\.py a" | awk '{print "kill "$2}')
|
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
tail -f ssserver.log
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,656 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 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.
|
||||
|
||||
# SOCKS5 UDP Request
|
||||
# +----+------+------+----------+----------+----------+
|
||||
# |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA |
|
||||
# +----+------+------+----------+----------+----------+
|
||||
# | 2 | 1 | 1 | Variable | 2 | Variable |
|
||||
# +----+------+------+----------+----------+----------+
|
||||
|
||||
# SOCKS5 UDP Response
|
||||
# +----+------+------+----------+----------+----------+
|
||||
# |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA |
|
||||
# +----+------+------+----------+----------+----------+
|
||||
# | 2 | 1 | 1 | Variable | 2 | Variable |
|
||||
# +----+------+------+----------+----------+----------+
|
||||
|
||||
# shadowsocks UDP Request (before encrypted)
|
||||
# +------+----------+----------+----------+
|
||||
# | ATYP | DST.ADDR | DST.PORT | DATA |
|
||||
# +------+----------+----------+----------+
|
||||
# | 1 | Variable | 2 | Variable |
|
||||
# +------+----------+----------+----------+
|
||||
|
||||
# shadowsocks UDP Response (before encrypted)
|
||||
# +------+----------+----------+----------+
|
||||
# | ATYP | DST.ADDR | DST.PORT | DATA |
|
||||
# +------+----------+----------+----------+
|
||||
# | 1 | Variable | 2 | Variable |
|
||||
# +------+----------+----------+----------+
|
||||
|
||||
# shadowsocks UDP Request and Response (after encrypted)
|
||||
# +-------+--------------+
|
||||
# | IV | PAYLOAD |
|
||||
# +-------+--------------+
|
||||
# | Fixed | Variable |
|
||||
# +-------+--------------+
|
||||
|
||||
# HOW TO NAME THINGS
|
||||
# ------------------
|
||||
# `dest` means destination server, which is from DST fields in the SOCKS5
|
||||
# request
|
||||
# `local` means local server of shadowsocks
|
||||
# `remote` means remote server of shadowsocks
|
||||
# `client` means UDP clients that connects to other servers
|
||||
# `server` means the UDP server that handles user requests
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import time
|
||||
import socket
|
||||
import logging
|
||||
import struct
|
||||
import errno
|
||||
import random
|
||||
import binascii
|
||||
import traceback
|
||||
import threading
|
||||
|
||||
from shadowsocks import encrypt, obfs, eventloop, lru_cache, common, shell
|
||||
from shadowsocks.common import pre_parse_header, parse_header, pack_addr
|
||||
|
||||
# for each handler, we have 2 stream directions:
|
||||
# upstream: from client to server direction
|
||||
# read local and write to remote
|
||||
# downstream: from server to client direction
|
||||
# read remote and write to local
|
||||
|
||||
STREAM_UP = 0
|
||||
STREAM_DOWN = 1
|
||||
|
||||
# for each stream, it's waiting for reading, or writing, or both
|
||||
WAIT_STATUS_INIT = 0
|
||||
WAIT_STATUS_READING = 1
|
||||
WAIT_STATUS_WRITING = 2
|
||||
WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING
|
||||
|
||||
BUF_SIZE = 65536
|
||||
DOUBLE_SEND_BEG_IDS = 16
|
||||
POST_MTU_MIN = 500
|
||||
POST_MTU_MAX = 1400
|
||||
SENDING_WINDOW_SIZE = 8192
|
||||
|
||||
STAGE_INIT = 0
|
||||
STAGE_RSP_ID = 1
|
||||
STAGE_DNS = 2
|
||||
STAGE_CONNECTING = 3
|
||||
STAGE_STREAM = 4
|
||||
STAGE_DESTROYED = -1
|
||||
|
||||
CMD_CONNECT = 0
|
||||
CMD_RSP_CONNECT = 1
|
||||
CMD_CONNECT_REMOTE = 2
|
||||
CMD_RSP_CONNECT_REMOTE = 3
|
||||
CMD_POST = 4
|
||||
CMD_SYN_STATUS = 5
|
||||
CMD_POST_64 = 6
|
||||
CMD_SYN_STATUS_64 = 7
|
||||
CMD_DISCONNECT = 8
|
||||
|
||||
CMD_VER_STR = b"\x08"
|
||||
|
||||
RSP_STATE_EMPTY = b""
|
||||
RSP_STATE_REJECT = b"\x00"
|
||||
RSP_STATE_CONNECTED = b"\x01"
|
||||
RSP_STATE_CONNECTEDREMOTE = b"\x02"
|
||||
RSP_STATE_ERROR = b"\x03"
|
||||
RSP_STATE_DISCONNECT = b"\x04"
|
||||
RSP_STATE_REDIRECT = b"\x05"
|
||||
|
||||
def client_key(source_addr, server_af):
|
||||
# notice this is server af, not dest af
|
||||
return '%s:%s:%d' % (source_addr[0], source_addr[1], server_af)
|
||||
|
||||
class UDPRelay(object):
|
||||
def __init__(self, config, dns_resolver, is_local, stat_callback=None, stat_counter=None):
|
||||
self._config = config
|
||||
if config.get('connect_verbose_info', 0) > 0:
|
||||
common.connect_log = logging.info
|
||||
if is_local:
|
||||
self._listen_addr = config['local_address']
|
||||
self._listen_port = config['local_port']
|
||||
self._remote_addr = config['server']
|
||||
self._remote_port = config['server_port']
|
||||
else:
|
||||
self._listen_addr = config['server']
|
||||
self._listen_port = config['server_port']
|
||||
self._remote_addr = None
|
||||
self._remote_port = None
|
||||
self._dns_resolver = dns_resolver
|
||||
self._password = common.to_bytes(config['password'])
|
||||
self._method = config['method']
|
||||
self._timeout = config['timeout']
|
||||
self._is_local = is_local
|
||||
self._udp_cache_size = config['udp_cache']
|
||||
self._cache = lru_cache.LRUCache(timeout=config['udp_timeout'],
|
||||
close_callback=self._close_client_pair)
|
||||
self._cache_dns_client = lru_cache.LRUCache(timeout=10,
|
||||
close_callback=self._close_client_pair)
|
||||
self._client_fd_to_server_addr = {}
|
||||
#self._dns_cache = lru_cache.LRUCache(timeout=1800)
|
||||
self._eventloop = None
|
||||
self._closed = False
|
||||
self.server_transfer_ul = 0
|
||||
self.server_transfer_dl = 0
|
||||
self.server_users = {}
|
||||
self.server_user_transfer_ul = {}
|
||||
self.server_user_transfer_dl = {}
|
||||
|
||||
if common.to_bytes(config['protocol']) in obfs.mu_protocol():
|
||||
self._update_users(None, None)
|
||||
|
||||
self.protocol_data = obfs.obfs(config['protocol']).init_data()
|
||||
self._protocol = obfs.obfs(config['protocol'])
|
||||
server_info = obfs.server_info(self.protocol_data)
|
||||
server_info.host = self._listen_addr
|
||||
server_info.port = self._listen_port
|
||||
server_info.users = self.server_users
|
||||
server_info.protocol_param = config['protocol_param']
|
||||
server_info.obfs_param = ''
|
||||
server_info.iv = b''
|
||||
server_info.recv_iv = b''
|
||||
server_info.key_str = common.to_bytes(config['password'])
|
||||
server_info.key = encrypt.encrypt_key(self._password, self._method)
|
||||
server_info.head_len = 30
|
||||
server_info.tcp_mss = 1452
|
||||
server_info.buffer_size = BUF_SIZE
|
||||
server_info.overhead = 0
|
||||
self._protocol.set_server_info(server_info)
|
||||
|
||||
self._sockets = set()
|
||||
self._fd_to_handlers = {}
|
||||
self._reqid_to_hd = {}
|
||||
self._data_to_write_to_server_socket = []
|
||||
|
||||
self._timeout_cache = lru_cache.LRUCache(timeout=self._timeout,
|
||||
close_callback=self._close_tcp_client)
|
||||
|
||||
self._bind = config.get('out_bind', '')
|
||||
self._bindv6 = config.get('out_bindv6', '')
|
||||
self._ignore_bind_list = config.get('ignore_bind', [])
|
||||
|
||||
if 'forbidden_ip' in config:
|
||||
self._forbidden_iplist = config['forbidden_ip']
|
||||
else:
|
||||
self._forbidden_iplist = None
|
||||
if 'forbidden_port' in config:
|
||||
self._forbidden_portset = config['forbidden_port']
|
||||
else:
|
||||
self._forbidden_portset = None
|
||||
|
||||
addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0,
|
||||
socket.SOCK_DGRAM, socket.SOL_UDP)
|
||||
if len(addrs) == 0:
|
||||
raise Exception("can't get addrinfo for %s:%d" %
|
||||
(self._listen_addr, self._listen_port))
|
||||
af, socktype, proto, canonname, sa = addrs[0]
|
||||
server_socket = socket.socket(af, socktype, proto)
|
||||
server_socket.bind((self._listen_addr, self._listen_port))
|
||||
server_socket.setblocking(False)
|
||||
self._server_socket = server_socket
|
||||
self._stat_callback = stat_callback
|
||||
|
||||
def _get_a_server(self):
|
||||
server = self._config['server']
|
||||
server_port = self._config['server_port']
|
||||
if type(server_port) == list:
|
||||
server_port = random.choice(server_port)
|
||||
if type(server) == list:
|
||||
server = random.choice(server)
|
||||
logging.debug('chosen server: %s:%d', server, server_port)
|
||||
return server, server_port
|
||||
|
||||
def get_ud(self):
|
||||
return (self.server_transfer_ul, self.server_transfer_dl)
|
||||
|
||||
def get_users_ud(self):
|
||||
ret = (self.server_user_transfer_ul.copy(), self.server_user_transfer_dl.copy())
|
||||
return ret
|
||||
|
||||
def _update_users(self, protocol_param, acl):
|
||||
if protocol_param is None:
|
||||
protocol_param = self._config['protocol_param']
|
||||
param = common.to_bytes(protocol_param).split(b'#')
|
||||
if len(param) == 2:
|
||||
user_list = param[1].split(b',')
|
||||
if user_list:
|
||||
for user in user_list:
|
||||
items = user.split(b':')
|
||||
if len(items) == 2:
|
||||
user_int_id = int(items[0])
|
||||
uid = struct.pack('<I', user_int_id)
|
||||
if acl is not None and user_int_id not in acl:
|
||||
self.del_user(uid)
|
||||
else:
|
||||
passwd = items[1]
|
||||
self.add_user(uid, {'password':passwd})
|
||||
|
||||
def _update_user(self, id, passwd):
|
||||
uid = struct.pack('<I', id)
|
||||
self.add_user(uid, passwd)
|
||||
|
||||
def update_users(self, users):
|
||||
for uid in list(self.server_users.keys()):
|
||||
id = struct.unpack('<I', uid)[0]
|
||||
if id not in users:
|
||||
self.del_user(uid)
|
||||
for id in users:
|
||||
uid = struct.pack('<I', id)
|
||||
self.add_user(uid, users[id])
|
||||
|
||||
def add_user(self, uid, cfg): # user: binstr[4], passwd: str
|
||||
passwd = cfg['password']
|
||||
self.server_users[uid] = common.to_bytes(passwd)
|
||||
|
||||
def del_user(self, uid):
|
||||
if uid in self.server_users:
|
||||
del self.server_users[uid]
|
||||
|
||||
def add_transfer_u(self, user, transfer):
|
||||
if user is None:
|
||||
self.server_transfer_ul += transfer
|
||||
else:
|
||||
if user not in self.server_user_transfer_ul:
|
||||
self.server_user_transfer_ul[user] = 0
|
||||
self.server_user_transfer_ul[user] += transfer + self.server_transfer_ul
|
||||
self.server_transfer_ul = 0
|
||||
|
||||
def add_transfer_d(self, user, transfer):
|
||||
if user is None:
|
||||
self.server_transfer_dl += transfer
|
||||
else:
|
||||
if user not in self.server_user_transfer_dl:
|
||||
self.server_user_transfer_dl[user] = 0
|
||||
self.server_user_transfer_dl[user] += transfer + self.server_transfer_dl
|
||||
self.server_transfer_dl = 0
|
||||
|
||||
def _close_client_pair(self, client_pair):
|
||||
client, uid = client_pair
|
||||
self._close_client(client)
|
||||
|
||||
def _close_client(self, client):
|
||||
if hasattr(client, 'close'):
|
||||
if not self._is_local:
|
||||
if client.fileno() in self._client_fd_to_server_addr:
|
||||
logging.debug('close_client: %s' %
|
||||
(self._client_fd_to_server_addr[client.fileno()],))
|
||||
else:
|
||||
client.info('close_client')
|
||||
self._sockets.remove(client.fileno())
|
||||
self._eventloop.remove(client)
|
||||
del self._client_fd_to_server_addr[client.fileno()]
|
||||
client.close()
|
||||
else:
|
||||
# just an address
|
||||
client.info('close_client pass %s' % client)
|
||||
pass
|
||||
|
||||
def _handel_protocol_error(self, client_address, ogn_data):
|
||||
#raise Exception('can not parse header')
|
||||
logging.warn("Protocol ERROR, UDP ogn data %s from %s:%d" % (binascii.hexlify(ogn_data), client_address[0], client_address[1]))
|
||||
|
||||
def _socket_bind_addr(self, sock, af):
|
||||
bind_addr = ''
|
||||
if self._bind and af == socket.AF_INET:
|
||||
bind_addr = self._bind
|
||||
elif self._bindv6 and af == socket.AF_INET6:
|
||||
bind_addr = self._bindv6
|
||||
|
||||
bind_addr = bind_addr.replace("::ffff:", "")
|
||||
if bind_addr in self._ignore_bind_list:
|
||||
bind_addr = None
|
||||
if bind_addr:
|
||||
local_addrs = socket.getaddrinfo(bind_addr, 0, 0, socket.SOCK_DGRAM, socket.SOL_UDP)
|
||||
if local_addrs[0][0] == af:
|
||||
logging.debug("bind %s" % (bind_addr,))
|
||||
try:
|
||||
sock.bind((bind_addr, 0))
|
||||
except Exception as e:
|
||||
logging.warn("bind %s fail" % (bind_addr,))
|
||||
|
||||
def _handle_server(self):
|
||||
server = self._server_socket
|
||||
data, r_addr = server.recvfrom(BUF_SIZE)
|
||||
ogn_data = data
|
||||
if not data:
|
||||
logging.debug('UDP handle_server: data is empty')
|
||||
if self._stat_callback:
|
||||
self._stat_callback(self._listen_port, len(data))
|
||||
uid = None
|
||||
if self._is_local:
|
||||
frag = common.ord(data[2])
|
||||
if frag != 0:
|
||||
logging.warn('drop a message since frag is not 0')
|
||||
return
|
||||
else:
|
||||
data = data[3:]
|
||||
else:
|
||||
ref_iv = [0]
|
||||
data = encrypt.encrypt_all_iv(self._protocol.obfs.server_info.key, self._method, 0, data, ref_iv)
|
||||
# decrypt data
|
||||
if not data:
|
||||
logging.debug('UDP handle_server: data is empty after decrypt')
|
||||
return
|
||||
self._protocol.obfs.server_info.recv_iv = ref_iv[0]
|
||||
data, uid = self._protocol.server_udp_post_decrypt(data)
|
||||
|
||||
#logging.info("UDP data %s" % (binascii.hexlify(data),))
|
||||
if not self._is_local:
|
||||
data = pre_parse_header(data)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
try:
|
||||
header_result = parse_header(data)
|
||||
except:
|
||||
self._handel_protocol_error(r_addr, ogn_data)
|
||||
return
|
||||
|
||||
if header_result is None:
|
||||
self._handel_protocol_error(r_addr, ogn_data)
|
||||
return
|
||||
connecttype, addrtype, dest_addr, dest_port, header_length = header_result
|
||||
|
||||
if self._is_local:
|
||||
addrtype = 3
|
||||
server_addr, server_port = self._get_a_server()
|
||||
else:
|
||||
server_addr, server_port = dest_addr, dest_port
|
||||
|
||||
if (addrtype & 7) == 3:
|
||||
af = common.is_ip(server_addr)
|
||||
if af == False:
|
||||
handler = common.UDPAsyncDNSHandler((data, r_addr, uid, header_length))
|
||||
handler.resolve(self._dns_resolver, (server_addr, server_port), self._handle_server_dns_resolved)
|
||||
else:
|
||||
self._handle_server_dns_resolved("", (server_addr, server_port), server_addr, (data, r_addr, uid, header_length))
|
||||
else:
|
||||
self._handle_server_dns_resolved("", (server_addr, server_port), server_addr, (data, r_addr, uid, header_length))
|
||||
|
||||
def _handle_server_dns_resolved(self, error, remote_addr, server_addr, params):
|
||||
if error:
|
||||
return
|
||||
data, r_addr, uid, header_length = params
|
||||
user_id = self._listen_port
|
||||
try:
|
||||
server_port = remote_addr[1]
|
||||
addrs = socket.getaddrinfo(server_addr, server_port, 0,
|
||||
socket.SOCK_DGRAM, socket.SOL_UDP)
|
||||
if not addrs: # drop
|
||||
return
|
||||
af, socktype, proto, canonname, sa = addrs[0]
|
||||
server_addr = sa[0]
|
||||
key = client_key(r_addr, af)
|
||||
client_pair = self._cache.get(key, None)
|
||||
if client_pair is None:
|
||||
client_pair = self._cache_dns_client.get(key, None)
|
||||
if client_pair is None:
|
||||
if self._forbidden_iplist:
|
||||
if common.to_str(sa[0]) in self._forbidden_iplist:
|
||||
logging.debug('IP %s is in forbidden list, drop' % common.to_str(sa[0]))
|
||||
# drop
|
||||
return
|
||||
if self._forbidden_portset:
|
||||
if sa[1] in self._forbidden_portset:
|
||||
logging.debug('Port %d is in forbidden list, reject' % sa[1])
|
||||
# drop
|
||||
return
|
||||
client = socket.socket(af, socktype, proto)
|
||||
client_uid = uid
|
||||
client.setblocking(False)
|
||||
self._socket_bind_addr(client, af)
|
||||
is_dns = False
|
||||
if len(data) > header_length + 13 and data[header_length + 4 : header_length + 12] == b"\x00\x01\x00\x00\x00\x00\x00\x00":
|
||||
is_dns = True
|
||||
else:
|
||||
pass
|
||||
if sa[1] == 53 and is_dns: #DNS
|
||||
logging.debug("DNS query %s from %s:%d" % (common.to_str(sa[0]), r_addr[0], r_addr[1]))
|
||||
self._cache_dns_client[key] = (client, uid)
|
||||
else:
|
||||
self._cache[key] = (client, uid)
|
||||
self._client_fd_to_server_addr[client.fileno()] = (r_addr, af)
|
||||
|
||||
self._sockets.add(client.fileno())
|
||||
self._eventloop.add(client, eventloop.POLL_IN, self)
|
||||
|
||||
logging.debug('UDP port %5d sockets %d' % (self._listen_port, len(self._sockets)))
|
||||
|
||||
if uid is not None:
|
||||
user_id = struct.unpack('<I', client_uid)[0]
|
||||
else:
|
||||
client, client_uid = client_pair
|
||||
self._cache.clear(self._udp_cache_size)
|
||||
self._cache_dns_client.clear(16)
|
||||
|
||||
if self._is_local:
|
||||
ref_iv = [encrypt.encrypt_new_iv(self._method)]
|
||||
self._protocol.obfs.server_info.iv = ref_iv[0]
|
||||
data = self._protocol.client_udp_pre_encrypt(data)
|
||||
#logging.debug("%s" % (binascii.hexlify(data),))
|
||||
data = encrypt.encrypt_all_iv(self._protocol.obfs.server_info.key, self._method, 1, data, ref_iv)
|
||||
if not data:
|
||||
return
|
||||
else:
|
||||
data = data[header_length:]
|
||||
if not data:
|
||||
return
|
||||
except Exception as e:
|
||||
shell.print_exception(e)
|
||||
logging.error("exception from user %d" % (user_id,))
|
||||
|
||||
try:
|
||||
client.sendto(data, (server_addr, server_port))
|
||||
self.add_transfer_u(client_uid, len(data))
|
||||
if client_pair is None: # new request
|
||||
addr, port = client.getsockname()[:2]
|
||||
common.connect_log('UDP data to %s(%s):%d from %s:%d by user %d' %
|
||||
(common.to_str(remote_addr[0]), common.to_str(server_addr), server_port, addr, port, user_id))
|
||||
except IOError as e:
|
||||
err = eventloop.errno_from_exception(e)
|
||||
logging.warning('IOError sendto %s:%d by user %d' % (server_addr, server_port, user_id))
|
||||
if err in (errno.EINPROGRESS, errno.EAGAIN):
|
||||
pass
|
||||
else:
|
||||
shell.print_exception(e)
|
||||
|
||||
def _handle_client(self, sock):
|
||||
data, r_addr = sock.recvfrom(BUF_SIZE)
|
||||
if not data:
|
||||
logging.debug('UDP handle_client: data is empty')
|
||||
return
|
||||
if self._stat_callback:
|
||||
self._stat_callback(self._listen_port, len(data))
|
||||
|
||||
client_addr = self._client_fd_to_server_addr.get(sock.fileno())
|
||||
client_uid = None
|
||||
if client_addr:
|
||||
key = client_key(client_addr[0], client_addr[1])
|
||||
client_pair = self._cache.get(key, None)
|
||||
client_dns_pair = self._cache_dns_client.get(key, None)
|
||||
if client_pair:
|
||||
client, client_uid = client_pair
|
||||
elif client_dns_pair:
|
||||
client, client_uid = client_dns_pair
|
||||
|
||||
if not self._is_local:
|
||||
addrlen = len(r_addr[0])
|
||||
if addrlen > 255:
|
||||
# drop
|
||||
return
|
||||
data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data
|
||||
ref_iv = [encrypt.encrypt_new_iv(self._method)]
|
||||
self._protocol.obfs.server_info.iv = ref_iv[0]
|
||||
data = self._protocol.server_udp_pre_encrypt(data, client_uid)
|
||||
response = encrypt.encrypt_all_iv(self._protocol.obfs.server_info.key, self._method, 1,
|
||||
data, ref_iv)
|
||||
if not response:
|
||||
return
|
||||
else:
|
||||
ref_iv = [0]
|
||||
data = encrypt.encrypt_all_iv(self._protocol.obfs.server_info.key, self._method, 0,
|
||||
data, ref_iv)
|
||||
if not data:
|
||||
return
|
||||
self._protocol.obfs.server_info.recv_iv = ref_iv[0]
|
||||
data = self._protocol.client_udp_post_decrypt(data)
|
||||
header_result = parse_header(data)
|
||||
if header_result is None:
|
||||
return
|
||||
#connecttype, dest_addr, dest_port, header_length = header_result
|
||||
#logging.debug('UDP handle_client %s:%d to %s:%d' % (common.to_str(r_addr[0]), r_addr[1], dest_addr, dest_port))
|
||||
|
||||
response = b'\x00\x00\x00' + data
|
||||
|
||||
if client_addr:
|
||||
if client_uid:
|
||||
self.add_transfer_d(client_uid, len(response))
|
||||
else:
|
||||
self.server_transfer_dl += len(response)
|
||||
self.write_to_server_socket(response, client_addr[0])
|
||||
if client_dns_pair:
|
||||
logging.debug("remove dns client %s:%d" % (client_addr[0][0], client_addr[0][1]))
|
||||
del self._cache_dns_client[key]
|
||||
self._close_client(client_dns_pair[0])
|
||||
else:
|
||||
# this packet is from somewhere else we know
|
||||
# simply drop that packet
|
||||
pass
|
||||
|
||||
def write_to_server_socket(self, data, addr):
|
||||
uncomplete = False
|
||||
retry = 0
|
||||
try:
|
||||
self._server_socket.sendto(data, addr)
|
||||
data = None
|
||||
while self._data_to_write_to_server_socket:
|
||||
data_buf = self._data_to_write_to_server_socket[0]
|
||||
retry = data_buf[1] + 1
|
||||
del self._data_to_write_to_server_socket[0]
|
||||
data, addr = data_buf[0]
|
||||
self._server_socket.sendto(data, addr)
|
||||
except (OSError, IOError) as e:
|
||||
error_no = eventloop.errno_from_exception(e)
|
||||
uncomplete = True
|
||||
if error_no in (errno.EWOULDBLOCK,):
|
||||
pass
|
||||
else:
|
||||
shell.print_exception(e)
|
||||
return False
|
||||
#if uncomplete and data is not None and retry < 3:
|
||||
# self._data_to_write_to_server_socket.append([(data, addr), retry])
|
||||
#'''
|
||||
|
||||
def add_to_loop(self, loop):
|
||||
if self._eventloop:
|
||||
raise Exception('already add to loop')
|
||||
if self._closed:
|
||||
raise Exception('already closed')
|
||||
self._eventloop = loop
|
||||
|
||||
server_socket = self._server_socket
|
||||
self._eventloop.add(server_socket,
|
||||
eventloop.POLL_IN | eventloop.POLL_ERR, self)
|
||||
loop.add_periodic(self.handle_periodic)
|
||||
|
||||
def remove_handler(self, client):
|
||||
if hash(client) in self._timeout_cache:
|
||||
del self._timeout_cache[hash(client)]
|
||||
|
||||
def update_activity(self, client):
|
||||
self._timeout_cache[hash(client)] = client
|
||||
|
||||
def _sweep_timeout(self):
|
||||
self._timeout_cache.sweep()
|
||||
|
||||
def _close_tcp_client(self, client):
|
||||
if client.remote_address:
|
||||
logging.debug('timed out: %s:%d' %
|
||||
client.remote_address)
|
||||
else:
|
||||
logging.debug('timed out')
|
||||
client.destroy()
|
||||
client.destroy_local()
|
||||
|
||||
def handle_event(self, sock, fd, event):
|
||||
if sock == self._server_socket:
|
||||
if event & eventloop.POLL_ERR:
|
||||
logging.error('UDP server_socket err')
|
||||
try:
|
||||
self._handle_server()
|
||||
except Exception as e:
|
||||
shell.print_exception(e)
|
||||
if self._config['verbose']:
|
||||
traceback.print_exc()
|
||||
elif sock and (fd in self._sockets):
|
||||
if event & eventloop.POLL_ERR:
|
||||
logging.error('UDP client_socket err')
|
||||
try:
|
||||
self._handle_client(sock)
|
||||
except Exception as e:
|
||||
shell.print_exception(e)
|
||||
if self._config['verbose']:
|
||||
traceback.print_exc()
|
||||
else:
|
||||
if sock:
|
||||
handler = self._fd_to_handlers.get(fd, None)
|
||||
if handler:
|
||||
handler.handle_event(sock, event)
|
||||
else:
|
||||
logging.warn('poll removed fd')
|
||||
|
||||
def handle_periodic(self):
|
||||
if self._closed:
|
||||
self._cache.clear(0)
|
||||
self._cache_dns_client.clear(0)
|
||||
if self._eventloop:
|
||||
self._eventloop.remove_periodic(self.handle_periodic)
|
||||
self._eventloop.remove(self._server_socket)
|
||||
if self._server_socket:
|
||||
self._server_socket.close()
|
||||
self._server_socket = None
|
||||
logging.info('closed UDP port %d', self._listen_port)
|
||||
else:
|
||||
before_sweep_size = len(self._sockets)
|
||||
self._cache.sweep()
|
||||
self._cache_dns_client.sweep()
|
||||
if before_sweep_size != len(self._sockets):
|
||||
logging.debug('UDP port %5d sockets %d' % (self._listen_port, len(self._sockets)))
|
||||
self._sweep_timeout()
|
||||
|
||||
def close(self, next_tick=False):
|
||||
logging.debug('UDP close')
|
||||
self._closed = True
|
||||
if not next_tick:
|
||||
if self._eventloop:
|
||||
self._eventloop.remove_periodic(self.handle_periodic)
|
||||
self._eventloop.remove(self._server_socket)
|
||||
self._server_socket.close()
|
||||
self._cache.clear(0)
|
||||
self._cache_dns_client.clear(0)
|
@ -0,0 +1,240 @@
|
||||
#coding:utf-8
|
||||
|
||||
import base64
|
||||
import re
|
||||
|
||||
class miss:
|
||||
def ssr4(self,ssrurl):
|
||||
missing_padding = 4 - len(ssrurl) % 4
|
||||
if missing_padding:
|
||||
ssrurl += b'='* missing_padding
|
||||
return ssrurl
|
||||
|
||||
def checkip(ip):
|
||||
p = re.compile('^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$')
|
||||
if p.match(ip):
|
||||
return False
|
||||
else:
|
||||
print "输入错误请重新输入"
|
||||
return True
|
||||
|
||||
def checkport(port):
|
||||
try:
|
||||
if int(port)<0 or int(port)>65535:
|
||||
print "输入错误请重新输入"
|
||||
return True
|
||||
return False
|
||||
except:
|
||||
print "输入错误请重新输入"
|
||||
return True
|
||||
|
||||
print '''
|
||||
请输入SSR地址码
|
||||
(就是ssr://XX,复制粘贴不嫌长您了就手打)
|
||||
手动配置服务器信息请输入1'''
|
||||
|
||||
error = True
|
||||
while error:
|
||||
ssrhead = raw_input()
|
||||
if ssrhead == "1":
|
||||
break
|
||||
try:
|
||||
ssrhead = re.split('[:/]',ssrhead)
|
||||
ssrurl = miss()
|
||||
ssrurl = ssrurl.ssr4(ssrhead[3])
|
||||
ssrurl = base64.urlsafe_b64decode(ssrurl)
|
||||
ssrurl = re.split('[:/?&]',ssrurl)
|
||||
serverip = ssrurl[0]
|
||||
serverport = ssrurl[1]
|
||||
password = ssrurl[5]
|
||||
password = miss()
|
||||
password = password.ssr4(ssrurl[5])
|
||||
password = base64.urlsafe_b64decode(password)
|
||||
method = ssrurl[3]
|
||||
protocol = ssrurl[2]
|
||||
obfs = ssrurl[4]
|
||||
error = False
|
||||
except:
|
||||
print "导入失败,请输入正确的SSR地址或输入1手动配置"
|
||||
error = True
|
||||
|
||||
if ssrhead == "1":
|
||||
error = True
|
||||
while error:
|
||||
serverip = raw_input("请输入服务器IP地址:\n")
|
||||
error = checkip(serverip)
|
||||
error = True
|
||||
while error:
|
||||
serverport = raw_input("请输入服务器端口:\n")
|
||||
error = checkport(serverport)
|
||||
error = True
|
||||
while error:
|
||||
print "请输入本机代理地址,默认127.0.0.1,使用默认请回车"
|
||||
localaddress = raw_input()
|
||||
if localaddress == "":
|
||||
localaddress = "127.0.0.1"
|
||||
error = checkip(localaddress)
|
||||
error = True
|
||||
while error:
|
||||
print "请输入本机代理端口,默认1080,使用默认请回车"
|
||||
localport = raw_input()
|
||||
if localport == "":
|
||||
localport = "1080"
|
||||
error = checkport(localport)
|
||||
password = raw_input("请输入密码:\n")
|
||||
|
||||
print '''
|
||||
0="NONE不加密"
|
||||
1="table"
|
||||
2="rc4"
|
||||
3="rc4-md5"
|
||||
4="rc4-md5-6"
|
||||
5="aes-128-cfb"
|
||||
6="aes-192-cfb"
|
||||
7="aes-256-cfb"
|
||||
8="aes-128-ctr"
|
||||
9="aes-192-ctr"
|
||||
10="aes-256-ctr"
|
||||
11="bf-cfb"
|
||||
12="camellia-128-cfb"
|
||||
13="camellia-192-cfb"
|
||||
14="camellia-256-cfb"
|
||||
15="cast5-cfb"
|
||||
16="des-cfb"
|
||||
17="idea-cfb"
|
||||
18="rc2-cfb"
|
||||
19="seed-cfb"
|
||||
20="salsa20"
|
||||
21="chacha20"
|
||||
22="chacha20-ietf"
|
||||
|
||||
请输入对应的加密方式数字'''
|
||||
method = ["","table","rc4","rc4-md5","rc4-md5-6","aes-128-cfb"\
|
||||
,"aes-192-cfb","aes-256-cfb","aes-128-ctr","aes-192-ctr","aes-256-ctr"\
|
||||
,"bf-cfb","camellia-128-cfb","camellia-192-cfb","camellia-256-cfb"\
|
||||
,"cast5-cfb","des-cfb","idea-cfb","rc2-cfb","seed-cfb","salsa20"\
|
||||
,"chacha20","chacha20-ietf"]
|
||||
error = True
|
||||
while error:
|
||||
try:
|
||||
num = input()
|
||||
if num < 0:
|
||||
num = "错误"
|
||||
method = method[num]
|
||||
error = False
|
||||
except:
|
||||
print "输入错误,请输入正确的数字"
|
||||
error = True
|
||||
|
||||
print '''
|
||||
1="origin"
|
||||
2="verify_simple"
|
||||
3="verify_sha1"
|
||||
4="auth_sha1"
|
||||
5="auth_sha1_v2"
|
||||
6="auth_sha1_v4"
|
||||
7="auth_aes128_sha1"
|
||||
8="auth_aes128_md5"
|
||||
9="auth_chain_a"
|
||||
10="auth_chain_b"
|
||||
11="auth_chain_c"
|
||||
12="auth_chain_d"
|
||||
|
||||
请输入对应的协议插件数字'''
|
||||
protocol = ["origin","verify_simple","verify_sha1","auth_sha1","auth_sha1_v2","auth_sha1_v4"\
|
||||
,"auth_aes128_sha1","auth_aes128_md5","auth_chain_a","auth_chain_b","auth_chain_c","auth_chain_d"]
|
||||
error = True
|
||||
while error:
|
||||
try:
|
||||
num = input()
|
||||
if num < 1:
|
||||
num = "错误"
|
||||
protocol = protocol[num-1]
|
||||
error = False
|
||||
except:
|
||||
print "请输入正确的数字"
|
||||
error = True
|
||||
|
||||
protocolparam = raw_input("请输入协议参数,不使用参数请回车:\n")
|
||||
|
||||
print '''
|
||||
1="plain"
|
||||
2="http_simple"
|
||||
3="http_post"
|
||||
4="tls_simple"
|
||||
5="tls1.2_ticket_auth"
|
||||
6="tls1.2_ticket_fastauth"
|
||||
|
||||
请输入对应的混淆参数的数字'''
|
||||
obfs = ["plain","http_simple","http_post","tls_simple"\
|
||||
,"tls1.2_ticket_auth","tls1.2_ticket_fastauth"]
|
||||
error = True
|
||||
while error:
|
||||
try:
|
||||
num = input()
|
||||
if num < 1:
|
||||
num = "错误"
|
||||
obfs = obfs[num-1]
|
||||
error = False
|
||||
except:
|
||||
print "请输入正确的数字"
|
||||
error = True
|
||||
|
||||
print '''
|
||||
请输入混淆参数
|
||||
示例:baidu.com (不需要加http)
|
||||
不使用参数请回车'''
|
||||
obfsparam = raw_input()
|
||||
|
||||
else:
|
||||
error = True
|
||||
while error:
|
||||
print "请输入本机代理地址,默认127.0.0.1,使用默认请回车"
|
||||
localaddress = raw_input()
|
||||
if localaddress == "":
|
||||
localaddress = "127.0.0.1"
|
||||
error = checkip(localaddress)
|
||||
error = True
|
||||
while error:
|
||||
print "请输入本机代理端口,默认1080,使用默认请回车"
|
||||
localport = raw_input()
|
||||
if localport == "":
|
||||
localport = "1080"
|
||||
error = checkport(localport)
|
||||
protocolparam = raw_input("请输入协议参数,不使用参数请回车:\n")
|
||||
print '''
|
||||
请输入混淆参数
|
||||
示例:baidu.com (不需要加http)
|
||||
不使用参数请回车'''
|
||||
obfsparam = raw_input()
|
||||
|
||||
user='''{
|
||||
"server": "%s",
|
||||
"server_ipv6": "::",
|
||||
"server_port": %s,
|
||||
"local_address": "%s",
|
||||
"local_port": %s,
|
||||
|
||||
"password": "%s",
|
||||
"method": "%s",
|
||||
"protocol": "%s",
|
||||
"protocol_param": "%s",
|
||||
"obfs": "%s",
|
||||
"obfs_param": "%s",
|
||||
"speed_limit_per_con": 0,
|
||||
"speed_limit_per_user": 0,
|
||||
|
||||
"additional_ports" : {},
|
||||
"timeout": 120,
|
||||
"udp_timeout": 60,
|
||||
"dns_ipv6": false,
|
||||
"connect_verbose_info": 0,
|
||||
"redirect": "",
|
||||
"fast_open": false
|
||||
}
|
||||
'''%(serverip,serverport,localaddress,localport,\
|
||||
password,method,protocol,protocolparam,obfs,obfsparam)
|
||||
with open('user-config.json','w') as f:
|
||||
f.write(user.encode("utf-8"))
|
||||
print user+"\n"+"请检查输入是否有误,若需要修改请重新执行程序。\
|
||||
\n启动ssr请在终端切换至shadowsocksr/shadowsocks目录执行python3 local.py -d start"
|
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2017 breakwa11
|
||||
#
|
||||
# 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.
|
||||
|
||||
def version():
|
||||
return '3.4.0 2017-07-27'
|
||||
|
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# python3_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1)
|
||||
eval $(ps -ef | grep "[0-9] python3 server\\.py m" | awk '{print "kill "$2}')
|
@ -0,0 +1,8 @@
|
||||
def getKeys(key_list):
|
||||
return key_list
|
||||
#return key_list + ['plan'] # append the column name 'plan'
|
||||
|
||||
def isTurnOn(row):
|
||||
return True
|
||||
#return row['plan'] == 'B' # then judge here
|
||||
|
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd `dirname $0`
|
||||
tail -f ssserver.log
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"127.0.0.1",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"aes_password",
|
||||
"timeout":60,
|
||||
"method":"aes-256-cfb1",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"127.0.0.1",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"aes_password",
|
||||
"timeout":60,
|
||||
"method":"aes-256-cfb8",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"127.0.0.1",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"aes_password",
|
||||
"timeout":60,
|
||||
"method":"aes-256-ctr",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"127.0.0.1",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"aes_password",
|
||||
"timeout":60,
|
||||
"method":"aes-256-cfb",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
# assert.sh 1.0 - bash unit testing framework
|
||||
# Copyright (C) 2009, 2010, 2011, 2012 Robert Lehmann
|
||||
#
|
||||
# http://github.com/lehmannro/assert.sh
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export DISCOVERONLY=${DISCOVERONLY:-}
|
||||
export DEBUG=${DEBUG:-}
|
||||
export STOP=${STOP:-}
|
||||
export INVARIANT=${INVARIANT:-}
|
||||
export CONTINUE=${CONTINUE:-}
|
||||
|
||||
args="$(getopt -n "$0" -l \
|
||||
verbose,help,stop,discover,invariant,continue vhxdic $*)" \
|
||||
|| exit -1
|
||||
for arg in $args; do
|
||||
case "$arg" in
|
||||
-h)
|
||||
echo "$0 [-vxidc]" \
|
||||
"[--verbose] [--stop] [--invariant] [--discover] [--continue]"
|
||||
echo "`sed 's/./ /g' <<< "$0"` [-h] [--help]"
|
||||
exit 0;;
|
||||
--help)
|
||||
cat <<EOF
|
||||
Usage: $0 [options]
|
||||
Language-agnostic unit tests for subprocesses.
|
||||
|
||||
Options:
|
||||
-v, --verbose generate output for every individual test case
|
||||
-x, --stop stop running tests after the first failure
|
||||
-i, --invariant do not measure timings to remain invariant between runs
|
||||
-d, --discover collect test suites only, do not run any tests
|
||||
-c, --continue do not modify exit code to test suite status
|
||||
-h show brief usage information and exit
|
||||
--help show this help message and exit
|
||||
EOF
|
||||
exit 0;;
|
||||
-v|--verbose)
|
||||
DEBUG=1;;
|
||||
-x|--stop)
|
||||
STOP=1;;
|
||||
-i|--invariant)
|
||||
INVARIANT=1;;
|
||||
-d|--discover)
|
||||
DISCOVERONLY=1;;
|
||||
-c|--continue)
|
||||
CONTINUE=1;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf -v _indent "\n\t" # local format helper
|
||||
|
||||
_assert_reset() {
|
||||
tests_ran=0
|
||||
tests_failed=0
|
||||
tests_errors=()
|
||||
tests_starttime="$(date +%s.%N)" # seconds_since_epoch.nanoseconds
|
||||
}
|
||||
|
||||
assert_end() {
|
||||
# assert_end [suite ..]
|
||||
tests_endtime="$(date +%s.%N)"
|
||||
tests="$tests_ran ${*:+$* }tests"
|
||||
[[ -n "$DISCOVERONLY" ]] && echo "collected $tests." && _assert_reset && return
|
||||
[[ -n "$DEBUG" ]] && echo
|
||||
[[ -z "$INVARIANT" ]] && report_time=" in $(bc \
|
||||
<<< "${tests_endtime%.N} - ${tests_starttime%.N}" \
|
||||
| sed -e 's/\.\([0-9]\{0,3\}\)[0-9]*/.\1/' -e 's/^\./0./')s" \
|
||||
|| report_time=
|
||||
|
||||
if [[ "$tests_failed" -eq 0 ]]; then
|
||||
echo "all $tests passed$report_time."
|
||||
else
|
||||
for error in "${tests_errors[@]}"; do echo "$error"; done
|
||||
echo "$tests_failed of $tests failed$report_time."
|
||||
fi
|
||||
tests_failed_previous=$tests_failed
|
||||
[[ $tests_failed -gt 0 ]] && tests_suite_status=1
|
||||
_assert_reset
|
||||
return $tests_failed_previous
|
||||
}
|
||||
|
||||
assert() {
|
||||
# assert <command> <expected stdout> [stdin]
|
||||
(( tests_ran++ )) || :
|
||||
[[ -n "$DISCOVERONLY" ]] && return || true
|
||||
# printf required for formatting
|
||||
printf -v expected "x${2:-}" # x required to overwrite older results
|
||||
result="$(eval 2>/dev/null $1 <<< ${3:-})" || true
|
||||
# Note: $expected is already decorated
|
||||
if [[ "x$result" == "$expected" ]]; then
|
||||
[[ -n "$DEBUG" ]] && echo -n . || true
|
||||
return
|
||||
fi
|
||||
result="$(sed -e :a -e '$!N;s/\n/\\n/;ta' <<< "$result")"
|
||||
[[ -z "$result" ]] && result="nothing" || result="\"$result\""
|
||||
[[ -z "$2" ]] && expected="nothing" || expected="\"$2\""
|
||||
_assert_fail "expected $expected${_indent}got $result" "$1" "$3"
|
||||
}
|
||||
|
||||
assert_raises() {
|
||||
# assert_raises <command> <expected code> [stdin]
|
||||
(( tests_ran++ )) || :
|
||||
[[ -n "$DISCOVERONLY" ]] && return || true
|
||||
status=0
|
||||
(eval $1 <<< ${3:-}) > /dev/null 2>&1 || status=$?
|
||||
expected=${2:-0}
|
||||
if [[ "$status" -eq "$expected" ]]; then
|
||||
[[ -n "$DEBUG" ]] && echo -n . || true
|
||||
return
|
||||
fi
|
||||
_assert_fail "program terminated with code $status instead of $expected" "$1" "$3"
|
||||
}
|
||||
|
||||
_assert_fail() {
|
||||
# _assert_fail <failure> <command> <stdin>
|
||||
[[ -n "$DEBUG" ]] && echo -n X
|
||||
report="test #$tests_ran \"$2${3:+ <<< $3}\" failed:${_indent}$1"
|
||||
if [[ -n "$STOP" ]]; then
|
||||
[[ -n "$DEBUG" ]] && echo
|
||||
echo "$report"
|
||||
exit 1
|
||||
fi
|
||||
tests_errors[$tests_failed]="$report"
|
||||
(( tests_failed++ )) || :
|
||||
}
|
||||
|
||||
_assert_reset
|
||||
: ${tests_suite_status:=0} # remember if any of the tests failed so far
|
||||
_assert_cleanup() {
|
||||
local status=$?
|
||||
# modify exit code if it's not already non-zero
|
||||
[[ $status -eq 0 && -z $CONTINUE ]] && exit $tests_suite_status
|
||||
}
|
||||
trap _assert_cleanup EXIT
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"127.0.0.1",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"salsa20_password",
|
||||
"timeout":60,
|
||||
"method":"chacha20",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":["127.0.0.1", "127.0.0.1"],
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"aes_password",
|
||||
"timeout":60,
|
||||
"method":"aes-256-cfb",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 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.
|
||||
|
||||
if __name__ == '__main__':
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
import urllib
|
||||
|
||||
class MainHandler(tornado.web.RequestHandler):
|
||||
def get(self, project):
|
||||
try:
|
||||
with open('/tmp/%s-coverage' % project, 'rb') as f:
|
||||
coverage = f.read().strip()
|
||||
n = int(coverage.strip('%'))
|
||||
if n >= 80:
|
||||
color = 'brightgreen'
|
||||
else:
|
||||
color = 'yellow'
|
||||
self.redirect(('https://img.shields.io/badge/'
|
||||
'coverage-%s-%s.svg'
|
||||
'?style=flat') %
|
||||
(urllib.quote(coverage), color))
|
||||
except IOError:
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
application = tornado.web.Application([
|
||||
(r"/([a-zA-Z0-9\-_]+)", MainHandler),
|
||||
])
|
||||
|
||||
if __name__ == "__main__":
|
||||
application.listen(8888, address='127.0.0.1')
|
||||
tornado.ioloop.IOLoop.instance().start()
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"127.0.0.1",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"fastopen_password",
|
||||
"timeout":60,
|
||||
"method":"aes-256-cfb",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":true
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"::1",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"aes_password",
|
||||
"timeout":60,
|
||||
"method":"aes-256-cfb",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"::",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"aes_password",
|
||||
"timeout":60,
|
||||
"method":"aes-256-cfb",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
|
||||
result=0
|
||||
|
||||
function run_test {
|
||||
printf '\e[0;36m'
|
||||
echo "running test: $command $@"
|
||||
printf '\e[0m'
|
||||
|
||||
$command "$@"
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
printf '\e[0;31m'
|
||||
echo "test failed: $command $@"
|
||||
printf '\e[0m'
|
||||
echo
|
||||
result=1
|
||||
else
|
||||
printf '\e[0;32m'
|
||||
echo OK
|
||||
printf '\e[0m'
|
||||
echo
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
python3 --version
|
||||
coverage erase
|
||||
mkdir tmp
|
||||
run_test pep8 --ignore=E402 .
|
||||
run_test pyflakes .
|
||||
run_test coverage run tests/nose_plugin.py -v
|
||||
run_test python3 setup.py sdist
|
||||
run_test tests/test_daemon.sh
|
||||
run_test python3 tests/test.py --with-coverage -c tests/aes.json
|
||||
run_test python3 tests/test.py --with-coverage -c tests/aes-ctr.json
|
||||
run_test python3 tests/test.py --with-coverage -c tests/aes-cfb1.json
|
||||
run_test python3 tests/test.py --with-coverage -c tests/aes-cfb8.json
|
||||
run_test python3 tests/test.py --with-coverage -c tests/rc4-md5.json
|
||||
run_test python3 tests/test.py --with-coverage -c tests/salsa20.json
|
||||
run_test python3 tests/test.py --with-coverage -c tests/chacha20.json
|
||||
run_test python3 tests/test.py --with-coverage -c tests/table.json
|
||||
run_test python3 tests/test.py --with-coverage -c tests/server-multi-ports.json
|
||||
run_test python3 tests/test.py --with-coverage -s tests/aes.json -c tests/client-multi-server-ip.json
|
||||
run_test python3 tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json
|
||||
run_test python3 tests/test.py --with-coverage -c tests/workers.json
|
||||
run_test python3 tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json
|
||||
run_test python3 tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -q" -a "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -vv"
|
||||
run_test python3 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 python3 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 if DNS works
|
||||
run_test python3 tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204"
|
||||
|
||||
# test localhost is in the forbidden list by default
|
||||
run_test python3 tests/test.py --with-coverage --should-fail --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1"
|
||||
|
||||
# test localhost is available when forbidden list is empty
|
||||
run_test python3 tests/test.py --with-coverage --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1"
|
||||
|
||||
if [ -f /proc/sys/net/ipv4/tcp_fastopen ] ; then
|
||||
if [ 3 -eq `cat /proc/sys/net/ipv4/tcp_fastopen` ] ; then
|
||||
# we have to run it twice:
|
||||
# the first time there's no syn cookie
|
||||
# the second time there is syn cookie
|
||||
run_test python3 tests/test.py --with-coverage -c tests/fastopen.json
|
||||
run_test python3 tests/test.py --with-coverage -c tests/fastopen.json
|
||||
fi
|
||||
fi
|
||||
|
||||
run_test tests/test_large_file.sh
|
||||
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
|
||||
|
||||
exit $result
|
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -d libsodium-1.0.1 ]; then
|
||||
wget https://github.com/jedisct1/libsodium/releases/download/1.0.1/libsodium-1.0.1.tar.gz || exit 1
|
||||
tar xf libsodium-1.0.1.tar.gz || exit 1
|
||||
fi
|
||||
pushd libsodium-1.0.1
|
||||
./configure && make -j2 && make install || exit 1
|
||||
sudo ldconfig
|
||||
popd
|
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 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.
|
||||
|
||||
import nose
|
||||
from nose.plugins.base import Plugin
|
||||
|
||||
|
||||
class ExtensionPlugin(Plugin):
|
||||
|
||||
name = "ExtensionPlugin"
|
||||
|
||||
def options(self, parser, env):
|
||||
Plugin.options(self, parser, env)
|
||||
|
||||
def configure(self, options, config):
|
||||
Plugin.configure(self, options, config)
|
||||
self.enabled = True
|
||||
|
||||
def wantFile(self, file):
|
||||
return file.endswith('.py')
|
||||
|
||||
def wantDirectory(self, directory):
|
||||
return True
|
||||
|
||||
def wantModule(self, file):
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
nose.main(addplugins=[ExtensionPlugin()])
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"127.0.0.1",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"aes_password",
|
||||
"timeout":60,
|
||||
"method":"rc4-md5",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"127.0.0.1",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"salsa20_password",
|
||||
"timeout":60,
|
||||
"method":"salsa20-ctr",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server":"127.0.0.1",
|
||||
"server_port":8388,
|
||||
"local_port":1081,
|
||||
"password":"salsa20_password",
|
||||
"timeout":60,
|
||||
"method":"salsa20",
|
||||
"local_address":"127.0.0.1",
|
||||
"fast_open":false
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"server": "127.0.0.1",
|
||||
"server_port": "8385",
|
||||
"local_port": 1081,
|
||||
"password": "foobar5",
|
||||
"timeout": 60,
|
||||
"method": "aes-256-cfb"
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
{
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 8384,
|
||||
"local_port": 1081,
|
||||
"password": "foobar4",
|
||||
"port_password": {
|
||||
"8381": "foobar1",
|
||||
"8382": "foobar2",
|
||||
"8383": "foobar3",
|
||||
"8384": "foobar4",
|
||||
"8385": "foobar5",
|
||||
"8386": "foobar6",
|
||||
"8387": "foobar7",
|
||||
"8388": "foobar8",
|
||||
"8389": "foobar9"
|
||||
},
|
||||
"timeout": 60,
|
||||
"method": "table"
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
{
|
||||
"server": "127.0.0.1",
|
||||
"local_port": 1081,
|
||||
"port_password": {
|
||||
"8381": "foobar1",
|
||||
"8382": "foobar2",
|
||||
"8383": "foobar3",
|
||||
"8384": "foobar4",
|
||||
"8385": "foobar5",
|
||||
"8386": "foobar6",
|
||||
"8387": "foobar7",
|
||||
"8388": "foobar8",
|
||||
"8389": "foobar9"
|
||||
},
|
||||
"timeout": 60,
|
||||
"method": "aes-256-cfb"
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"server": "127.0.0.1",
|
||||
"server_port": [8384, 8345, 8346, 8347],
|
||||
"local_port": 1081,
|
||||
"password": "foobar4",
|
||||
"timeout": 60,
|
||||
"method": "aes-256-cfb"
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
DEV=lo
|
||||
PORT=8388
|
||||
DELAY=100ms
|
||||
|
||||
type tc 2> /dev/null && (
|
||||
tc qdisc add dev $DEV root handle 1: htb
|
||||
tc class add dev $DEV parent 1: classid 1:1 htb rate 2mbps
|
||||
tc class add dev $DEV parent 1:1 classid 1:6 htb rate 2mbps ceil 1mbps prio 0
|
||||
tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 6 fw flowid 1:6
|
||||
|
||||
tc filter add dev $DEV parent 1:0 protocol ip u32 match ip dport $PORT 0xffff flowid 1:6
|
||||
tc filter add dev $DEV parent 1:0 protocol ip u32 match ip sport $PORT 0xffff flowid 1:6
|
||||
|
||||
tc qdisc show dev lo
|
||||
)
|
||||
|
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -d dante-1.4.0 ]; then
|
||||
wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1
|
||||
tar xf dante-1.4.0.tar.gz || exit 1
|
||||
fi
|
||||
pushd dante-1.4.0
|
||||
./configure && make -j4 && make install || exit 1
|
||||
popd
|
||||
cp tests/socksify/socks.conf /etc/ || exit 1
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user