zhenkyle/shadowsocks_ruby

View on GitHub
lib/shadowsocks_ruby/protocols/obfs/tls_ticket.rb

Summary

Maintainability
B
6 hrs
Test Coverage
require 'lrucache'

module ShadowsocksRuby
  module Protocols
    # TLS 1.2 Obfuscation Protocol
    #
    # Specification:
    # * https://github.com/shadowsocksr/shadowsocksr/blob/manyuser/shadowsocks/obfsplugin/obfs_tls.py
    # * https://github.com/shadowsocksr/obfsplugin/blob/master/c/tls1.2_ticket.c
    #
    # TLS 1.2 Reference:
    # * https://en.wikipedia.org/wiki/Transport_Layer_Security
    # * https://tools.ietf.org/html/rfc5246
    # * https://tools.ietf.org/html/rfc5077
    # * https://tools.ietf.org/html/rfc6066
    class TlsTicketProtocol
      include DummyHelper
      include BufferHelper

      VERSION_SSL_3_0 = [3, 0]
      VERSION_TLS_1_0 = [3, 1]
      VERSION_TLS_1_1 = [3, 2]
      VERSION_TLS_1_2 = [3, 3]

      #Content types
      CTYPE_ChangeCipherSpec = 0x14
      CTYPE_Alert = 0x15
      CTYPE_Handshake = 0x16
      CTYPE_Application = 0x17
      CTYPE_Heartbeat = 0x18

      #Message types
      MTYPE_HelloRequest = 0 
      MTYPE_ClientHello = 1
      MTYPE_ServerHello = 2
      MTYPE_NewSessionTicket = 4
      MTYPE_Certificate = 11
      MTYPE_ServerKeyExchange = 12
      MTYPE_CertificateRequest = 13
      MTYPE_ServerHelloDone = 14
      MTYPE_CertificateVerify = 15
      MTYPE_ClientKeyExchange = 16
      MTYPE_Finished = 20

      attr_accessor :next_protocol

      # @param [Hash] params                                Configuration parameters
      # @option params [String]                :host        shadowsocks server address, required by remoteserver protocol
      # @option params [String]                :key         key, required by both remoteserver and localbackend protocol
      # @option params [Boolean]               :compatible  compatibility with origin mode, default _true_
      # @option params [String]                :obfs_param  obfs param, optional
      # @option params [LRUCache]              :lrucache    lrucache, optional, it intened to be a lrucache Proxy if provided
      def initialize params = {}
        @params = {:compatible => true}.merge(params)
        @buffer = ''
        @client_id = Random.new.bytes(32)
        @max_time_dif = 60 * 60 * 24 # time dif (second) setting
        @startup_time = Time.now.to_i - 60 * 30
        @client_data = @params[:lrucache] || LRUCache.new(:ttl => 60 * 5)
        @connected = EM::DefaultDeferrable.new
      end

      def tcp_send_to_remoteserver_first_packet data
        @connected.callback { send_client_change_cipherspec_and_finish data }
        class << self
          alias tcp_send_to_remoteserver tcp_send_to_remoteserver_other_packet
        end

      end

      alias tcp_send_to_remoteserver tcp_send_to_remoteserver_first_packet

      # TLS 1.2 Application Pharse
      def tcp_send_to_remoteserver_other_packet data
        @connected.callback { send_data_application_pharse data }
      end

      def tcp_receive_from_remoteserver_first_packet n
        send_client_hello
        recv_server_hello
        @connected.succeed
        class << self
          alias tcp_receive_from_remoteserver tcp_receive_from_remoteserver_other_packet
        end
        tcp_receive_from_remoteserver_other_packet n
      end

      alias tcp_receive_from_remoteserver tcp_receive_from_remoteserver_first_packet

      # TLS 1.2 Application Pharse
      def tcp_receive_from_remoteserver_other_packet n
        head = async_recv 3
        if head != [CTYPE_Application, *VERSION_TLS_1_2].pack("C3")
          raise PharseError, "client_decode appdata error"
        end
        size = async_recv(2).unpack("n")[0]
        @buffer << async_recv(size)

        tcp_receive_from_remoteserver_other_packet_helper n
      end



      def tcp_receive_from_localbackend_first_packet n
        class << self
          alias tcp_receive_from_localbackend tcp_receive_from_localbackend_other_packet
        end
        recv_client_hello
        @no_effect ||= nil
        if !@no_effect
          send_server_hello
          recv_client_change_cipherspec_and_finish
          tcp_receive_from_localbackend_other_packet n
        else
          tcp_receive_from_localbackend_other_packet_helper n
        end
      end

      alias tcp_receive_from_localbackend tcp_receive_from_localbackend_first_packet

      # TLS 1.2 Application Pharse
      def tcp_receive_from_localbackend_other_packet n
        @no_effect ||= nil
        if !@no_effect
          begin
            head = async_recv 3
            if head != [CTYPE_Application, *VERSION_TLS_1_2].pack("C3")
              raise PharseError, "server_decode appdata error"
            end
            size = async_recv(2).unpack("n")[0]
          end while size == 0
          @buffer << async_recv(size)
        end

        tcp_receive_from_localbackend_other_packet_helper n
      end

      def tcp_send_to_localbackend data
        @no_effect ||= nil
        if !@no_effect
          send_data_application_pharse data
        else
          send_data data
        end
      end

      # helpers
      def get_random
        verifyid = [Time.now.to_i].pack("N") << Random.new.bytes(18)
        hello = ""
        hello << verifyid # Random part 1
        hello << ShadowsocksRuby::Cipher.hmac_sha1_digest(@params[:key] + @client_id, verifyid) # Random part 2
      end

      def send_client_hello
        client_hello = ""
        
        client_hello << [*VERSION_TLS_1_2].pack("C2") # ProtocolVersion
        client_hello << get_random # Random len 32
        client_hello << [32].pack("C") << @client_id # SessionID
        client_hello << Util.hex2bin("001cc02bc02fcca9cca8cc14cc13c00ac014c009c013009c0035002f000a") # CipherSuite
        client_hello << Util.hex2bin("0100") # CompressionMethod

        ext = Util.hex2bin("ff01000100") # Extension 1 (type ff01 + len 0001 + data 00 )
        
        hosts = @params[:obfs_param] || @params[:host]
        if (hosts == nil or hosts == "")
          raise PharseError, "No :host or :obfs_param parameters"
        end
        if (("0".."9").include? hosts[-1])
          hosts = ""
        end
        hosts = hosts.split(",")
        if hosts.length != 0
          host = hosts[Random.rand(hosts.length)]
        else
          host = ""
        end
        ext << make_ext_sni(host) # Extension 2
        ext << Util.hex2bin("00170000") # Extension 3 (type 0017 + len 0000)
        ext << Util.hex2bin("002300d0") << Random.new.bytes(208) # ticket, Extension 4 (type 0023 + len 00d0 + data)
        ext << Util.hex2bin("000d001600140601060305010503040104030301030302010203") # Extension 5 (type 000d + len 0016 + data)
        ext << Util.hex2bin("000500050100000000") # Extension 6 (type 0005 + len 0005 + data)
        ext << Util.hex2bin("00120000") # Extension 7 (type 0012 + len 0000)
        ext << Util.hex2bin("75500000") # Extension 8 (type 7550 + len 0000)
        ext << Util.hex2bin("000b00020100") # Extension 9 (type 000b + len 0002 + data)
        ext << Util.hex2bin("000a0006000400170018") # Extension 10 (type 000a + len 0006 + data)

        client_hello << [ext.length].pack("n") << ext # Extension List

        client_handshake_message = [MTYPE_ClientHello, 0, client_hello.length].pack("CCn") << client_hello
        handshake_message = [CTYPE_Handshake,*VERSION_TLS_1_0, client_handshake_message.length].pack("C3n") << client_handshake_message

        send_data(handshake_message)
      end

      def send_client_change_cipherspec_and_finish data
        buf = ""
        buf << [CTYPE_ChangeCipherSpec, *VERSION_TLS_1_2, 0, 1, 1].pack("C*")
        buf << [CTYPE_Handshake, *VERSION_TLS_1_2, 32].pack("C3n") << Random.new.bytes(22)
        buf << ShadowsocksRuby::Cipher::hmac_sha1_digest(@params[:key] + @client_id, buf)
        buf << [CTYPE_Application, *VERSION_TLS_1_2, data.length].pack("C3n") << data
        send_data buf
      end

      def recv_server_hello
        data = async_recv(129) # ServerHello 76 + ServerChangeSipherSpec 6 + Finished 37
        verify = data[11 ... 33]
        if ShadowsocksRuby::Cipher.hmac_sha1_digest(@params[:key] + @client_id, verify) != data[33 ... 43]
          raise PharseError, "client_decode data error"
        end
      end

      def recv_client_hello
        data = async_recv 3
        if data != [CTYPE_Handshake, *VERSION_TLS_1_0].pack("C3")
          if @params[:compatible]
            @buffer = data
            @no_effect = true
            return
          else
            raise PharseError, "decode error"
          end
        end

        len_client_handshake_message = async_recv(2).unpack("n")[0]
        client_handshake_message = async_recv(len_client_handshake_message)

        if (client_handshake_message.slice!(0, 2) != [MTYPE_ClientHello, 0].pack("C2"))
          raise PharseError, "tls_auth not client hello message"
        end

        len_client_hello = client_handshake_message.slice!(0, 2).unpack("n")[0]
        client_hello = client_handshake_message

        if (len_client_hello != client_hello.length )
          raise PharseError, "tls_auth wrong message size"
        end

        if (client_hello.slice!(0,2) != [*VERSION_TLS_1_2].pack("C2"))
          raise PharseError, "tls_auth wrong tls version"
        end

        verifyid = client_hello.slice!(0, 32)

        len_sessionid = client_hello.slice!(0,1).unpack("C")[0]
        if (len_sessionid < 32)
          raise PharseError, "tls_auth wrong sessionid_len"
        end

        sessionid = client_hello.slice!(0, len_sessionid)
        @client_id = sessionid

        sha1 = ShadowsocksRuby::Cipher::hmac_sha1_digest(@params[:key] + sessionid, verifyid[0, 22])
        utc_time = Time.at(verifyid[0, 4].unpack("N")[0])
        time_dif = Time.now.to_i - utc_time.to_i

        #if @params[:obfs_param] != nil
        #  @max_time_dif = @params[:obfs_param].to_i
        #end
        if @max_time_dif > 0 && (time_dif.abs > @max_time_dif or utc_time.to_i - @startup_time < - @max_time_dif / 2)
          raise PharseError, "tls_auth wrong time"
        end

        if sha1 != verifyid[22 .. -1]
          raise PharseError, "tls_auth wrong sha1"
        end

        if @client_data[verifyid[0, 22]]
          raise PharseError, "replay attack detect, id = #{Util.bin2hex(verifyid)}"
        end
        @client_data[verifyid[0, 22]] = sessionid
      end

      def send_server_hello
        data = [*VERSION_TLS_1_2].pack("C2")
        data << get_random
        data << Util.hex2bin("20") # len 32 in decimal
        data << @client_id
        data << Util.hex2bin("c02f000005ff01000100")
        data = Util.hex2bin("0200") << [data.length].pack("n") << data
        data = Util.hex2bin("160303") << [data.length].pack("n") << data # ServerHello len 86 (11 + 32 + 1 + 32 + 10)
        data << Util.hex2bin("14") << [*VERSION_TLS_1_2].pack("C2") << Util.hex2bin("000101") # ChangeCipherSpec len (6)
        data << Util.hex2bin("16") << [*VERSION_TLS_1_2].pack("C2") << Util.hex2bin("0020") << Random.new.bytes(22)
        data << ShadowsocksRuby::Cipher.hmac_sha1_digest(@params[:key] + @client_id, data) # Finished len(37)
        send_data data # len 129
      end

      def recv_client_change_cipherspec_and_finish
        data = async_recv 43
        if data[0, 6] !=  [CTYPE_ChangeCipherSpec, *VERSION_TLS_1_2, 0, 1, 1].pack("C*") # ChangeCipherSpec
          raise PharseError, "server_decode data error"
        end
        if data[6, 5] != [CTYPE_Handshake, *VERSION_TLS_1_2, 32].pack("C3n") # Finished
          raise PharseError, "server_decode data error"
        end
        if ShadowsocksRuby::Cipher.hmac_sha1_digest(@params[:key] + @client_id, data[0, 33]) != data[33, 10]
          raise PharseError, "server_decode data error"
        end
      end

      def send_data_application_pharse data
        buf = ""
        while data.length > 2048
          size = [Random.rand(65535) % 4096 + 100, data.length].min
          buf << [CTYPE_Application, *VERSION_TLS_1_2, size].pack("C3n") << data.slice!(0, size)
        end
        if data.length > 0
          buf << [CTYPE_Application, *VERSION_TLS_1_2, data.length].pack("C3n") << data
        end
        send_data buf
      end

      def make_ext_sni host
          name_type = 0 #host_name
          server_name = [name_type, host.length].pack("Cn") << host
          server_name_list = [server_name.length].pack("n") << server_name

          type = Util.hex2bin("0000")
          data = [server_name_list.length].pack("n") << server_name_list

          return type << data
      end

      alias tcp_receive_from_client raise_me
      alias tcp_send_to_client raise_me
      #alias tcp_receive_from_remoteserver raise_me
      #alias tcp_send_to_remoteserver raise_me
      #alias tcp_receive_from_localbackend raise_me
      #alias tcp_send_to_localbackend raise_me
      alias tcp_receive_from_destination raise_me
      alias tcp_send_to_destination raise_me

      alias udp_receive_from_client raise_me
      alias udp_send_to_client raise_me
      alias udp_receive_from_remoteserver raise_me
      alias udp_send_to_remoteserver raise_me
      alias udp_receive_from_localbackend raise_me
      alias udp_send_to_localbackend raise_me
      alias udp_receive_from_destination raise_me
      alias udp_send_to_destination raise_me
    end

  end
end