これは 【その2】ドリコム Advent Calendar 2015 24日目の記事です。
23日目の記事は hayato240 さんの二郎を思い浮かべながら、RSpecを学んだことの振り返り です。
【その1】ドリコム Advent Calendar 2015 もあわせてどうぞ。
自己紹介
こんにちは、タイガーです。
中国上海からきてます。ドリコム歴はもう4年目です。
本業はサーバーサイドのエンジニアです。今年は iOS と Android のクライアントの開発も結構やりました。
まえがき
この間 Great Firewall が VPN も遮断するニュースがありまして、ちょっと周りの人とその話をしました。
多分エンジニアの皆さんはご存知と思います。中国にいるならこれを越えないといろいろ不便になります。そのためにいろんなツールも作られています。一番有名のは shadowsocks 、見ての通りいつかその存在が消えるかもしれませんね。エンジニアとしてはやはり自分がその仕組を理解すれば一番いいと思います。
本題
Firewall を通過するには、SOCKS5 っていうプロトコルが存在します。具体的な仕様は RFC1928 に定義されています。8ページしかないので、割りとすぐ読み終えます。基本的は SOCKS5 をベースにして作ればいろいろ汎用出来ます。
すでに shadowsocks 大先輩があるので、それを参考にしながら作ってみましょう。言語はもちろん Ruby です。
大体な構造は下図の通り
まずはローカルサーバーとリモートサーバー2つの部分があります。
ブラウザとかの SOCKS5 をサポートしてるアプリケーションはローカルサーバーに接続します。
リモートサーバーは Firewall の外にあるサーバーで実行します。
Firewall の外にアクセスする時は ブラウザ → ローカルサーバー → リモートサーバー → 目標サーバー の流れになります。
ローカルサーバー
まず必要なのは並列処理です。ブラウザは同時にたくさんのアクセスがくるので、並列処理ができないと使い物にならないです。今の時代はもちろん Event Driven I/O でやります。Ruby なら EventMachine があります。
とりあえず EventMachine で実装してみました。ちょっと長いです。(´・ω・`)
local.rb link 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
require "eventmachine"
LOCAL_SERVER_HOST = "127.0.0.1"
LOCAL_SERVER_PORT = "8081"
REMOTE_SERVER_HOST = "127.0.0.1"
REMOTE_SERVER_PORT = "8082"
class LocalConnection < EventMachine :: Connection
attr_accessor :server
def send_encoded_data ( data )
return if data . nil? || data . length == 0
send_data ( data )
end
def receive_data ( data )
server . send_data ( data )
end
def unbind
server . close_connection_after_writing
end
end
module LocalServer
def post_init
@fiber = Fiber . new do
greeting
loop { do_command }
end
end
def receive_data ( data )
if @connection
@connection . send_encoded_data ( data . to_s )
else
@data = data
@fiber = nil if @fiber . resume
end
end
def unbind
@connection . close_connection if @connection
end
private
# IN
# +----+----------+----------+
# |VER | NMETHODS | METHODS |
# +----+----------+----------+
# | 1 | 1 | 1 to 255 |
# +----+----------+----------+
#
# OUT
# +----+--------+
# |VER | METHOD |
# +----+--------+
# | 1 | 1 |
# +----+--------+
def greeting
ver = @data . unpack ( "C" ) . first
clear_data
if ver == 5
send_data " \x05\x00 " # NO AUTHENTICATION REQUIRED
else
send_data " \x05\xFF " # NO ACCEPTABLE METHODS
end
Fiber . yield
end
# IN
# +----+-----+-------+------+----------+----------+
# |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
# +----+-----+-------+------+----------+----------+
# | 1 | 1 | X'00' | 1 | Variable | 2 |
# +----+-----+-------+------+----------+----------+
#
# OUT
# see the defination of reply_data
def do_command
_ , cmd , _ , atype , addr_length = @data . unpack ( "C5" )
header_length = 0
case atype
when 1 , 4 # 1: ipv4, 4 bytes / 4: ipv6, 16 bytes
ip_length = 4 * atype
host = IPAddr . ntop @data [ 4 , ip_length ]
port = @data [ 4 + ip_length , 2 ]. unpack ( 'S>' ) . first
header_length = ip_length + 6
when 3 # domain name
host = @data [ 5 , addr_length ]
port = @data [ 5 + addr_length , 2 ]. unpack ( 'S>' ) . first
header_length = addr_length + 7
else
panic :address_type_not_supported
end
case cmd
when 1
send_data reply_data ( :success )
@connection = EventMachine . connect ( REMOTE_SERVER_HOST , REMOTE_SERVER_PORT , LocalConnection )
@connection . server = self
@connection . send_encoded_data ( " #{ host } : #{ port } \n " )
@connection . send_encoded_data ( @data [ header_length , - 1 ] )
clear_data
Fiber . yield
when 2 , 3 # bind, udp
panic :command_not_supported
else
panic :command_not_supported
end
end
# +----+-----+-------+------+----------+----------+
# |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
# +----+-----+-------+------+----------+----------+
# | 1 | 1 | X'00' | 1 | Variable | 2 |
# +----+-----+-------+------+----------+----------+
def reply_data ( type )
@replies_hash ||= begin
{
success : 0 ,
command_not_supported : 7 ,
address_type_not_supported : 8 ,
} . map { | k , v | [ k , ( " \x05 #{ [ v ]. pack ( 'C' ) } \x00\x01\x00\x00\x00\x00\x00\x00 " ) ] } . to_h
end
@replies_hash [ type ]
end
def clear_data
@data = nil
end
def panic ( reply_type )
send_data reply_data ( reply_type )
Fiber . yield true
end
end
EventMachine . run do
puts "Start socks5 at #{ LOCAL_SERVER_HOST } : #{ LOCAL_SERVER_PORT } "
EventMachine . start_server ( LOCAL_SERVER_HOST , LOCAL_SERVER_PORT , LocalServer )
end
SOCKS5 の handshake をやるとき連続でデータのやり取りをするから、ここでは Fiber を使えば効率よくかつかっこ良く実装できます。
アクセスがきたら、SOCKS のバージョンは5だったら \x05\x00
を返します。(認証いらないから、NMETHODS と METHODS は無視)
次にコマンドのデータがきます。仕様通りホストとポートを解析します。
急いで書いたので、TCP しか実装していません。(´Д`)
解析したホストとポートと転送したいデータをリモートサーバーに送ります。
shadowsocks はローカルからリモートへ転送する時も SOCKS5 に従って実装してます。多分リモートサーバーの間に互換性をもたらすためです。こちらはシンプルのため、下記の形でデータを送ります。
<HOST>:<PORT>\nDATA...
リモートサーバー
ローカルサーバーに比べたらすごくシンプルなものです。
上記のホストとポートを取り出し、目標サーバーに接続し、データの部分をそのまま送ります。
最後は帰ってきたレスポンスを逆の経路でブラウザに返します。
remote.rb link 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
require "eventmachine"
REMOTE_SERVER_PORT = "8082"
class RemoteConnection < EventMachine :: Connection
attr_accessor :server
def receive_data ( data )
@server . send_encoded_data ( data )
end
def unbind
@server . close_connection_after_writing
end
end
class RemoteServer < EventMachine :: Connection
def post_init
@buffer = ""
end
def send_encoded_data ( data )
return if data . nil? || data . length == 0
# TODO: encode data
send_data ( data )
end
def receive_data ( data )
# TODO: decode data
if @buffer
@buffer << data
addr , rest = @buffer . split ( " \n " , 2 )
if rest && rest . length > 0
host , port = addr . split ( ":" )
port = port . nil? ? 80 : port . to_i
@connection = EventMachine . connect ( host , port , RemoteConnection )
@connection . server = self
@connection . send_data ( rest ) if rest . length > 0
@buffer = nil
end
else
@connection . send_data ( data ) if data && data . length > 0
end
rescue
@connection . close_connection if @connection
close_connection
end
def unbind
@connection . close_connection if @connection
end
end
EventMachine . run do
puts "Starting server at 0.0.0.0: #{ REMOTE_SERVER_PORT } "
EventMachine . start_server ( '0.0.0.0' , REMOTE_SERVER_PORT , RemoteServer )
end
使い方
リポジトリはこちらに上げました。socks-tunnel-demo
まずは一番上のホストとポートを設定します。
ローカルで bundle exec ruby local.rb
を実行します。
リモートサーバーで bundle exec ruby remote.rb
を実行します。
ブラウザのプロキシの設定にローカルサーバーを設定します。Mac ならこんな感じです。
これでブラウザはプロキシ経由で外にアクセスできるはずです。
TODO
ここまで作ったものは実際には全く使いものにならないです。orz
何故なら急いで書いたから、肝心な暗号化の部分はまだ実装されていません(TODO が書いてるところ)。
今のローカルからリモートへの通信は Firewall に丸見えです。簡単に探知され、ゲームオーバーになります。
暗号化については連休中で補完できればと思います。本当に申し訳ございません。m(_ _)m
明日
【その2】ドリコム Adevent Calendar 2015 25日目は ericinderbuchtvontokio さんです。