├── README.md ├── client └── .gitkeep └── server ├── 01_basic_server.rb ├── 02_basic_tcp_server.rb ├── 03_basic_interactive_tcp_server.rb ├── 04_basic_multiple_client_tcp_server.rb ├── 05_forked_multiple_client_tcp_server.rb ├── 06_eventmachine_multiple_client_tcp_server.rb ├── 07_eventmachine_socks5_server.rb ├── 08_stronger_eventmachine_socks5_server.rb └── 09_stateful_socks5_server.rb /README.md: -------------------------------------------------------------------------------- 1 | # Examples of Ruby network programming 2 | 3 | Example codes of Ruby's TCP related network programming, including: 4 | 5 | 1. basic classical style (bind-listen-accept-read-write-close) TCP server 6 | 2. basic TCP server with ruby's style 7 | 3. interactive TCP server 8 | 4. multiplexing a TCP server with either `Thread` or `Process` 9 | 5. basic TCP server with [EventMachine](https://github.com/eventmachine/eventmachine) 10 | 5. two versions of SOCKS5 server with [EventMachine](https://github.com/eventmachine/eventmachine) 11 | 5. adding your own logics to the SOCKS5 server 12 | 13 | [related slide](https://speakerdeck.com/qhwa/tcp-socket-network-programming-in-ruby) 14 | 15 | ## useful commands 16 | 17 | This command indicates connections bind to your interested port. Replace `PORT` with a real port number such as `23333`. 18 | 19 | ~~~sh 20 | watch -n 0 'netstat -nta | grep PORT' 21 | ~~~ 22 | -------------------------------------------------------------------------------- /client/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qhwa/ruby-network-programming/f2cb3d10a6b84c449c695b0de1910dfdad4d7314/client/.gitkeep -------------------------------------------------------------------------------- /server/01_basic_server.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'logger' 3 | 4 | # This program demos how to use ruby to create a basic 5 | # C-style (bind-listen-read-write-close) TCP Server 6 | class Server 7 | 8 | def start(bind: '0.0.0.0', port: nil, backlog: 10) 9 | sock = Socket.new(:INET, :STREAM) 10 | sock.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, true) 11 | 12 | # bind 13 | sock.bind Addrinfo.tcp bind, port 14 | 15 | # listen 16 | sock.listen backlog 17 | log { "start server %s:%d" % [sock.local_address.ip_address, sock.local_address.ip_port] } 18 | 19 | client, client_addr = sock.accept 20 | log { "client connected from %s:%d" % [client_addr.ip_address, client_addr.ip_port] } 21 | 22 | client.puts "Hello there! %s" % Time.now 23 | 24 | loop do 25 | # read 26 | input = client.gets.chomp 27 | if input == "q" 28 | log { "client disconnected from %s:%d" % [client_addr.ip_address, client_addr.ip_port] } 29 | break 30 | else 31 | # write 32 | client.puts input.reverse 33 | end 34 | end 35 | 36 | # close 37 | client.close 38 | ensure 39 | sock.close 40 | end 41 | 42 | def log(msg=nil, &block) 43 | Logger.new(STDOUT).debug msg, &block 44 | end 45 | 46 | end 47 | 48 | Server.new.start(port: 23333, bind: '127.0.0.1') 49 | -------------------------------------------------------------------------------- /server/02_basic_tcp_server.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'logger' 3 | 4 | class Server 5 | 6 | def start(bind: '0.0.0.0', port: nil) 7 | Socket.tcp_server_loop(bind, port) do |sock, client_addr| 8 | log { "client connected: %s:%d" % [client_addr.ip_address, client_addr.ip_port] } 9 | sock.puts Time.now 10 | sock.close 11 | end 12 | end 13 | 14 | def log(msg=nil, &block) 15 | Logger.new(STDOUT).debug msg, &block 16 | end 17 | 18 | end 19 | 20 | Server.new.start(port: 23333, bind: '127.0.0.1') 21 | -------------------------------------------------------------------------------- /server/03_basic_interactive_tcp_server.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'logger' 3 | 4 | class Server 5 | 6 | def start(bind: '0.0.0.0', port: nil) 7 | Socket.tcp_server_loop(bind, port) do |sock, client_addr| 8 | log { "client connected: %s:%d" % [client_addr.ip_address, client_addr.ip_port] } 9 | 10 | sock.puts "Welcome buddy!" 11 | 12 | loop do 13 | input = sock.gets.chomp 14 | if input == "q" 15 | sock.close 16 | break 17 | end 18 | sock.puts input.reverse 19 | end 20 | 21 | end 22 | end 23 | 24 | def log(msg=nil, &block) 25 | Logger.new(STDOUT).debug msg, &block 26 | end 27 | 28 | end 29 | 30 | Server.new.start(port: 23333, bind: '127.0.0.1') 31 | -------------------------------------------------------------------------------- /server/04_basic_multiple_client_tcp_server.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'logger' 3 | 4 | class Server 5 | 6 | def start(bind: '0.0.0.0', port: nil) 7 | Socket.tcp_server_loop(bind, port) do |sock, client_addr| 8 | Thread.new do 9 | log { "client connected: %s:%d" % [client_addr.ip_address, client_addr.ip_port] } 10 | 11 | sock.puts "Welcome buddy!" 12 | 13 | loop do 14 | input = sock.gets.chomp 15 | if input == "q" 16 | sock.close 17 | else 18 | sock.puts input.reverse 19 | end 20 | end 21 | end 22 | end 23 | end 24 | 25 | def log(msg=nil, &block) 26 | Logger.new(STDOUT).debug msg, &block 27 | end 28 | 29 | end 30 | 31 | Server.new.start(port: 23333, bind: '127.0.0.1') 32 | -------------------------------------------------------------------------------- /server/05_forked_multiple_client_tcp_server.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'logger' 3 | 4 | class Server 5 | 6 | def start(bind: '0.0.0.0', port: nil) 7 | Socket.tcp_server_loop(bind, port) do |sock, client_addr| 8 | 9 | if fork 10 | log { "client connected: %s:%d" % [client_addr.ip_address, client_addr.ip_port] } 11 | 12 | sock.puts "Welcome buddy!" 13 | 14 | loop do 15 | input = sock.gets.chomp 16 | if input == 'q' 17 | sock.close 18 | break 19 | else 20 | sock.puts input.reverse 21 | end 22 | end 23 | else 24 | sock.close 25 | end 26 | end 27 | end 28 | 29 | def log(msg=nil, &block) 30 | Logger.new(STDOUT).debug msg, &block 31 | end 32 | 33 | end 34 | 35 | Server.new.start(port: 23333, bind: '127.0.0.1') 36 | -------------------------------------------------------------------------------- /server/06_eventmachine_multiple_client_tcp_server.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | require 'logger' 3 | 4 | class Server 5 | 6 | def start(bind: '0.0.0.0', port: nil) 7 | EM.run { EM.start_server bind, port, Handler } 8 | end 9 | 10 | module Handler 11 | 12 | # Everytime a new client is connected, EventMachine will initialize 13 | # a new Connection class (or a class inheriting from Connection defined 14 | # by you). The instance of Connection class with automaticly invoke 15 | # `post_init` method 16 | def post_init 17 | log { "new client connected from: %s:%d" % client_addr } 18 | end 19 | 20 | # Everytime client send data to server, this method will be invoked 21 | def receive_data data 22 | data.chomp! 23 | if data == "q" 24 | close_connection 25 | else 26 | send_data data.reverse 27 | send_data "\n" 28 | end 29 | end 30 | 31 | def unbind 32 | log { "client disconnected from: %s:%d" % client_addr } 33 | end 34 | 35 | private 36 | 37 | def log(msg=nil, &block) 38 | Logger.new(STDOUT).debug msg, &block 39 | end 40 | 41 | def client_addr 42 | @client_addr ||= Socket.unpack_sockaddr_in(get_peername).reverse 43 | end 44 | 45 | end 46 | 47 | end 48 | 49 | Server.new.start(port: 23333, bind: '127.0.0.1') 50 | -------------------------------------------------------------------------------- /server/07_eventmachine_socks5_server.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | require 'logger' 3 | require 'stringio' 4 | 5 | class Server 6 | 7 | def start(bind: '0.0.0.0', port: nil) 8 | EM.run { EM.start_server bind, port, Socks5Handler } 9 | end 10 | 11 | # SOCKS5 proxy server 12 | # RFC#1928: https://www.ietf.org/rfc/rfc1928.txt 13 | module Socks5Handler 14 | 15 | # we only support a few socks5 features for demo only 16 | SOCKS_GREETING = "\x05\x02\x00\x01" 17 | SOCKS_GREETING_REPLY = "\x05\x00" 18 | SOCKS_CONNECT = "\x05\x01\x00" 19 | SOCKS_CONNECT_REPLY = "\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00" 20 | 21 | attr_accessor :dest_host, :dest_port, :proxy_conn, :data 22 | 23 | def post_init 24 | log { "new client connected from: %s:%d" % client_addr } 25 | end 26 | 27 | def receive_data data 28 | @data = data 29 | if proxy_ready? 30 | proxy_conn.send_data data 31 | else 32 | greeting or connect 33 | end 34 | end 35 | 36 | def proxy_ready? 37 | !!proxy_conn 38 | end 39 | 40 | def greeting 41 | send_data SOCKS_GREETING_REPLY if data == SOCKS_GREETING 42 | end 43 | 44 | def connect 45 | io = StringIO.new(data) 46 | if io.read(3) == SOCKS_CONNECT 47 | self.dest_host = case atype = io.getbyte 48 | when 1 49 | io.read(4).unpack("C*").join "." 50 | when 3 51 | io.read(io.getbyte) 52 | when 4 53 | io.read(16).unpack("n*").map {|i| i.to_s(16)}.join ":" 54 | end 55 | self.dest_port = io.read(2).unpack("n").first 56 | start_proxy 57 | 58 | log { "destination connected: %s:%d" % [dest_host, dest_port] } 59 | send_data SOCKS_CONNECT_REPLY 60 | end 61 | end 62 | 63 | def start_proxy 64 | 65 | proxy_handler = Module.new do 66 | 67 | def initialize client 68 | @client = client 69 | end 70 | 71 | def receive_data data 72 | @client.send_data data 73 | end 74 | 75 | def unbind 76 | @client.close_connection_after_writing 77 | end 78 | end 79 | 80 | @proxy_conn ||= EM.connect(dest_host, dest_port, proxy_handler, self) 81 | end 82 | 83 | def unbind 84 | log { "client disconnected from: %s:%d" % client_addr } 85 | end 86 | 87 | private 88 | 89 | def log(msg=nil, &block) 90 | Logger.new(STDOUT).debug msg, &block 91 | end 92 | 93 | def client_addr 94 | @client_addr ||= Socket.unpack_sockaddr_in(get_peername).reverse 95 | end 96 | 97 | end 98 | 99 | end 100 | 101 | Server.new.start(port: 23333, bind: '127.0.0.1') 102 | -------------------------------------------------------------------------------- /server/08_stronger_eventmachine_socks5_server.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | require 'logger' 3 | require 'stringio' 4 | 5 | class Server 6 | 7 | def start(bind: '0.0.0.0', port: nil) 8 | EM.run { EM.start_server bind, port, Socks5Handler } 9 | end 10 | 11 | # SOCKS5 proxy server 12 | # RFC#1928: https://www.ietf.org/rfc/rfc1928.txt 13 | # https://github.com/luikore/stochastic-socks/blob/master/local.rb 14 | module Socks5Handler 15 | 16 | # we only support a few socks5 features for demo only 17 | SOCKS_GREETING_REPLY = "\x05\x00" 18 | SOCKS_CONNECT = "\x05\x01\x00" 19 | SOCKS_CONNECT_REPLY = "\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00" 20 | 21 | attr_accessor :dest_host, :dest_port, :proxy_conn, :data 22 | 23 | def post_init 24 | log { "new client connected from: %s:%d" % client_addr } 25 | end 26 | 27 | def receive_data data 28 | if proxy_ready? 29 | proxy_conn.send_data data 30 | else 31 | @data ||= "" 32 | @data << data 33 | return greeting unless @greeted 34 | connect 35 | end 36 | end 37 | 38 | def proxy_ready? 39 | !!proxy_conn 40 | end 41 | 42 | def greeting 43 | return if data.bytesize < 3 44 | 45 | ver, len = data.byteslice(0, 2).unpack('c*') 46 | unless ver == 5 47 | panic 'unsuported socks version' 48 | return 49 | end 50 | 51 | @data = "" 52 | @greeted = true 53 | send_data SOCKS_GREETING_REPLY 54 | end 55 | 56 | def panic msg 57 | log msg 58 | send_data msg 59 | close_connection_after_writing 60 | end 61 | 62 | def connect 63 | return if data.bytesize < 6 64 | 65 | io = StringIO.new(data) 66 | if io.read(3) == SOCKS_CONNECT 67 | self.dest_host = case atype = io.getbyte 68 | when 1 69 | io.read(4).unpack("C*").join "." 70 | when 3 71 | io.read(io.getbyte) 72 | when 4 73 | io.read(16).unpack("n*").map {|i| i.to_s(16)}.join ":" 74 | end 75 | self.dest_port = io.read(2).unpack("n").first 76 | start_proxy 77 | 78 | log { "destination connected: %s:%d" % [dest_host, dest_port] } 79 | @data = "" 80 | send_data SOCKS_CONNECT_REPLY 81 | end 82 | end 83 | 84 | def wait n 85 | 86 | end 87 | 88 | private :wait 89 | 90 | def start_proxy 91 | 92 | proxy_handler = Module.new do 93 | 94 | def initialize client 95 | @client = client 96 | end 97 | 98 | def receive_data data 99 | @client.send_data data 100 | end 101 | 102 | def unbind 103 | @client.close_connection_after_writing 104 | end 105 | end 106 | 107 | @proxy_conn ||= EM.connect(dest_host, dest_port, proxy_handler, self) 108 | end 109 | 110 | def unbind 111 | log { "client disconnected from: %s:%d" % client_addr } 112 | end 113 | 114 | private 115 | 116 | def log(msg=nil, &block) 117 | Logger.new(STDOUT).debug msg, &block 118 | end 119 | 120 | def client_addr 121 | @client_addr ||= Socket.unpack_sockaddr_in(get_peername).reverse 122 | end 123 | 124 | end 125 | 126 | end 127 | 128 | Server.new.start(port: 23333, bind: '127.0.0.1') 129 | -------------------------------------------------------------------------------- /server/09_stateful_socks5_server.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | require 'logger' 3 | require 'stringio' 4 | require 'redis' 5 | 6 | class Server 7 | 8 | def start(bind: '0.0.0.0', port: nil) 9 | EM.run { EM.start_server bind, port, StatefulHandler } 10 | end 11 | 12 | # SOCKS5 proxy server 13 | # RFC#1928: https://www.ietf.org/rfc/rfc1928.txt 14 | # https://github.com/luikore/stochastic-socks/blob/master/local.rb 15 | module Socks5Handler 16 | 17 | # we only support a few socks5 features for demo only 18 | SOCKS_GREETING_REPLY = "\x05\x00" 19 | SOCKS_CONNECT = "\x05\x01\x00" 20 | SOCKS_CONNECT_REPLY = "\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00" 21 | 22 | attr_accessor :dest_host, :dest_port, :proxy_conn, :data 23 | 24 | def post_init 25 | log { "new client connected from: %s:%d" % client_addr } 26 | end 27 | 28 | def receive_data data 29 | if proxy_ready? 30 | proxy_conn.send_data data 31 | else 32 | @data ||= "" 33 | @data << data 34 | return greeting unless @greeted 35 | connect 36 | end 37 | end 38 | 39 | def proxy_ready? 40 | !!proxy_conn 41 | end 42 | 43 | def greeting 44 | return if data.bytesize < 3 45 | 46 | ver, len = data.byteslice(0, 2).unpack('c*') 47 | unless ver == 5 48 | panic 'unsuported socks version' 49 | return 50 | end 51 | 52 | @data = "" 53 | @greeted = true 54 | send_data SOCKS_GREETING_REPLY 55 | end 56 | 57 | def panic msg 58 | log msg 59 | send_data msg 60 | close_connection_after_writing 61 | end 62 | 63 | def connect 64 | return if data.bytesize < 6 65 | 66 | io = StringIO.new(data) 67 | if io.read(3) == SOCKS_CONNECT 68 | self.dest_host = case atype = io.getbyte 69 | when 1 70 | io.read(4).unpack("C*").join "." 71 | when 3 72 | io.read(io.getbyte) 73 | when 4 74 | io.read(16).unpack("n*").map {|i| i.to_s(16)}.join ":" 75 | end 76 | self.dest_port = io.read(2).unpack("n").first 77 | start_proxy 78 | 79 | log { "destination connected: %s:%d" % [dest_host, dest_port] } 80 | @data = "" 81 | send_data SOCKS_CONNECT_REPLY 82 | end 83 | end 84 | 85 | def wait n 86 | 87 | end 88 | 89 | private :wait 90 | 91 | def start_proxy 92 | 93 | proxy_handler = Module.new do 94 | 95 | def initialize client 96 | @client = client 97 | end 98 | 99 | def receive_data data 100 | @client.send_data data 101 | end 102 | 103 | def unbind 104 | @client.close_connection_after_writing 105 | end 106 | end 107 | 108 | @proxy_conn ||= EM.connect(dest_host, dest_port, proxy_handler, self) 109 | end 110 | 111 | def unbind 112 | log { "client disconnected from: %s:%d" % client_addr } 113 | end 114 | 115 | private 116 | 117 | def log(msg=nil, &block) 118 | Logger.new(STDOUT).debug msg, &block 119 | end 120 | 121 | def client_addr 122 | @client_addr ||= Socket.unpack_sockaddr_in(get_peername).reverse 123 | end 124 | 125 | end 126 | 127 | 128 | module StatefulHandler 129 | include Socks5Handler 130 | 131 | def connect 132 | ip, _ = client_addr 133 | 134 | # to allow: redis-cli set allowed:127.0.0.1 1 135 | # to disallow: redis-cli del allowed:127.0.0.1 136 | EM.defer -> { Redis.new.get "allowed:#{ip}" }, 137 | -> (allowed) { allowed ? super : close_connection } 138 | end 139 | 140 | end 141 | 142 | end 143 | 144 | Server.new.start(port: 23333, bind: '127.0.0.1') 145 | --------------------------------------------------------------------------------