Class: ShadowsocksRuby::App

Inherits:
Object
  • Object
show all
Includes:
Singleton
Defined in:
lib/shadowsocks_ruby/app.rb

Overview

App is a singleton object which provide either Shadowsocks Client functionality or Shadowsocks Server functionality. One App startup one EventMachine event loop on one Native Thread / CPU core.

Because Ruby MRI has a GIL, it is unable to utilize execution parallelism on multi-core CPU. However, one can use a shared socket manager to spin off a few Apps that can be executed parallelly and let the Kernel to do the load balance things.

A few things noticeable when using a shared socket manager:

  • At present, only Einhorn socket manager are supported. systemd shared socket manager support is in the plan.

  • At present, using socket manager could cause TLS1.2 obsfucation protocol’s replay attact detection malfunctions, because every process have it’s own copy of LRUCache.

  • Shared socket manager does not work on Windows

Constant Summary collapse

MAX_FAST_SHUTDOWN_SECONDS =
10
@@options =
{}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeApp

Returns a new instance of App.



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
# File 'lib/shadowsocks_ruby/app.rb', line 42

def initialize
  @options = @@options

  @totalcounter = 0
  @maxcounter = 0
  @counter = 0
  if options[:__server]
    name = "ssserver-ruby"
    host = options[:server]
    port = options[:port]
  elsif options[:__client]
    name = "sslocal-ruby"
    host = options[:local_addr]
    port = options[:local_port]
  end
  @name = name
  @listen = "#{host}:#{port}"

  update_procline


  @logger = Logger.new(STDOUT).tap do |log|
    log.datetime_format = '%Y-%m-%d %H:%M:%S '
    if options[:__server]
      log.progname = "ssserver"
    elsif options[:__client]
      log.progname = "sslocal"
    end

    if options[:verbose]
      log.level = Logger::DEBUG 
    elsif options[:quiet]
      log.level = Logger::WARN 
    else
      log.level = Logger::INFO
    end
  end

end

Instance Attribute Details

#loggerObject

Returns the value of attribute logger.



34
35
36
# File 'lib/shadowsocks_ruby/app.rb', line 34

def logger
  @logger
end

#optionsObject (readonly)

Returns the value of attribute options.



33
34
35
# File 'lib/shadowsocks_ruby/app.rb', line 33

def options
  @options
end

Class Method Details

.options=(options) ⇒ Object



38
39
40
# File 'lib/shadowsocks_ruby/app.rb', line 38

def self.options= options
  @@options = options
end

Instance Method Details

#decrObject



98
99
100
101
102
103
104
105
106
107
# File 'lib/shadowsocks_ruby/app.rb', line 98

def decr
  @counter -= 1
  @server ||= nil # quick fix for warning in connection unit test
  if @server.nil?
    logger.info "Waiting for #{@counter} connections to finish."
  end
  update_procline
  EventMachine.stop_event_loop if @server.nil? and @counter == 0
  @counter
end

#fast_shutdown(signal) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/shadowsocks_ruby/app.rb', line 144

def fast_shutdown(signal)
  EventMachine.stop_server(@server) if @server
  threads = []
  threads << Thread.new{ logger.info "Received #{signal} signal. No longer accepting new connections." }
  threads << Thread.new{ logger.info "Maximum time to wait for connections is #{MAX_FAST_SHUTDOWN_SECONDS} seconds." }
  threads << Thread.new{ logger.info "Waiting for #{@counter} connections to finish." }
  threads.each { |thr| thr.join }
  @server = nil
  EventMachine.stop_event_loop if @counter == 0
  Thread.new do
    sleep MAX_FAST_SHUTDOWN_SECONDS
    $kernel.exit!
  end
end

#get_cipher_protocolObject



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/shadowsocks_ruby/app.rb', line 231

def get_cipher_protocol
  if options[:cipher_name] == nil ||  options[:cipher_name] == "none"
    return nil
  end
  case options[:cipher_name]
  when "table"
    ["no_iv_cipher", {}]
  else
    case options[:packet_name]
    when "origin"
      ["iv_cipher", {}]
    when "verify_sha1"
      ["verify_sha1", {}]
    when "verify_sha1_strict"
      ["verify_sha1", {:compatible => false}]
    end
  end
end

#get_obfs_protocolObject



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/shadowsocks_ruby/app.rb', line 250

def get_obfs_protocol
  if options[:obfs_name] == nil
    return nil
  end
  case options[:obfs_name]
  when "http_simple", "http_simple_compatible"
    ["http_simple", {:host => options[:server], :port => options[:port], :obfs_param => options[:obfs_param]}]
  when "http_simple_strict"
    ["http_simple", {:host => options[:server], :port => options[:port], :obfs_param => options[:obfs_param], :compatible => false}]
  when "tls1.2_ticket_auth", "tls1.2_ticket_auth_compatible"
    ["tls_ticket", {:host => options[:server], :obfs_param => options[:obfs_param]}]
  when "tls1.2_ticket_auth_strict"
    ["tls_ticket", {:host => options[:server], :obfs_param => options[:obfs_param], :compatible => false}]
  else
    raise AppError, "no such protocol: #{options[:obfs_name]}"
  end
end

#get_packet_protocolObject



222
223
224
225
226
227
228
229
# File 'lib/shadowsocks_ruby/app.rb', line 222

def get_packet_protocol
  case options[:packet_name]
  when "origin", "verify_sha1", "verify_sha1_strict"
    ["shadowsocks", {}]
  else
    raise AppError, "no such protocol: #{options[:packet_name]}"
  end
end

#graceful_shutdown(signal) ⇒ Object



134
135
136
137
138
139
140
141
142
# File 'lib/shadowsocks_ruby/app.rb', line 134

def graceful_shutdown(signal)
  EventMachine.stop_server(@server) if @server
  threads = []
  threads << Thread.new{ logger.info "Received #{signal} signal. No longer accepting new connections." }
  threads << Thread.new{ logger.info "Waiting for #{@counter} connections to finish." }
  threads.each { |thr| thr.join }
  @server = nil
  EventMachine.stop_event_loop if @counter == 0
end

#incrObject



90
91
92
93
94
95
96
# File 'lib/shadowsocks_ruby/app.rb', line 90

def incr
  @totalcounter += 1
  @counter += 1
  @maxcounter = @counter if @counter > @maxcounter
  update_procline
  @counter
end

#run!Object



109
110
111
112
113
114
115
116
117
# File 'lib/shadowsocks_ruby/app.rb', line 109

def run!
  if options[:__server]
    start_server
  elsif options[:__client]
    start_client
  end
rescue => e
  logger.fatal e
end

#start_clientObject



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/shadowsocks_ruby/app.rb', line 181

def start_client
  stack1 = Protocols::ProtocolStack.new([
      ["socks5", {}]
  ], options[:cipher_name], options[:password])

  stack2 = Protocols::ProtocolStack.new([
    get_packet_protocol,
    get_cipher_protocol,
    get_obfs_protocol
  ].compact, options[:cipher_name], options[:password])

  local_args = [
    stack1,
    {:host => options[:server], :port => options[:port], :timeout => options[:timeout]},
    stack2,
    {:timeout => options[:timeout]}
  ]

  start_em options[:local_addr], options[:local_port], Connections::TCP::ClientConnection, local_args
end

#start_em(host, port, klass_server, server_args) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/shadowsocks_ruby/app.rb', line 202

def start_em host, port, klass_server, server_args
  EventMachine.epoll

  EventMachine.run do
    if options[:einhorn] != true
      @server = EventMachine.start_server(host, port, klass_server, *server_args)
    else
      fd_num = Einhorn::Worker.socket!
      socket = Socket.for_fd(fd_num)

      @server = EventMachine.attach_server(socket, klass_server, *server_args)
    end
    logger.info "Listening on #{host}:#{port}"
    logger.info "Send QUIT to quit after waiting for all connections to finish."
    logger.info "Send TERM or INT to quit after waiting for up to #{MAX_FAST_SHUTDOWN_SECONDS} seconds for connections to finish."

    trap_signals
  end
end

#start_serverObject



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/shadowsocks_ruby/app.rb', line 160

def start_server
  stack3 = Protocols::ProtocolStack.new([
    get_packet_protocol,
    get_cipher_protocol,
    get_obfs_protocol
  ].compact, options[:cipher_name], options[:password])

  stack4 = Protocols::ProtocolStack.new([
    ["plain", {}]
  ], options[:cipher_name], options[:password])

  server_args = [
    stack3,
    {:timeout => options[:timeout]},
    stack4,
    {:timeout => options[:timeout]}
  ]

  start_em options[:server], options[:port], Connections::TCP::LocalBackendConnection, server_args
end

#statsObject



86
87
88
# File 'lib/shadowsocks_ruby/app.rb', line 86

def stats
  "#{@counter}/#{@maxcounter}/#{@totalcounter}"
end

#trap_signalsObject



120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/shadowsocks_ruby/app.rb', line 120

def trap_signals
  STDOUT.sync = true
  STDERR.sync = true
  Signal.trap('QUIT') do
    graceful_shutdown('QUIT')
  end
  Signal.trap('TERM') do
    fast_shutdown('TERM')
  end
  Signal.trap('INT') do
    fast_shutdown('INT')
  end
end

#update_proclineObject



82
83
84
# File 'lib/shadowsocks_ruby/app.rb', line 82

def update_procline
  $0 = "shadowsocks_ruby #{VERSION} - #{@name} #{@listen} - #{stats} cur/max/tot conns"
end