require 'openssl' require 'socket' require 'zlib' # Public: Main knock class # # This class is responsible for computing a cryptographic sequence of ports to # knock, via UDP packets, to tickle a firewall to allow the source IP through. class CocKnocker # Public: Raised when 'generation' wraps 8 bits and none of the packets have # been deemed acceptable. This should, in practice, never occur unless you # have done something seriously wrong. class Unacceptable < StandardError; end # Public: See initializer documentation for descriptions of these attributes. attr_accessor :key, :tgt_ip, :my_ip # Public: A list of ports which should be avoided when knocking, due to their # special meaning or likelihood of being blocked by either source or target # internet service providers. BAD_PORTS = [ 0, 25, 135, 139, 445 ].freeze # Public: Packet encapsulation and handling class. # # Packet format is as follows: # uint8_t generation - Integer to alter packet; increment this to generate # a different packet in case the generated port list # contains items that are found in the BAD_PORTS list. # uint32_t ip - IP address of the node attempting to knock (the # 'client'), to mitigate replay attacks. # uint32_t time - UNIX Epoch timestamp of when the knock is occuring, # to mitigate replay attacks. # uint32_t crc - CRC32 checksum of the first 9 bytes of the encoded # representation of the packet, to ensure a successful # decode. # All values are encoded in network byte order. class Packet # Public: See class documentation for descriptions of these attributes. attr_accessor :generation, :ip, :time # Public: Compute the CRC32 checksum of the passed binary packet string, to # ensure that it matches what is encoded in the packet itself. # # string packet - Encoded packet to verify # # Returns true if the computed CRC32 checksum of the first 9 bytes of the # provided packet matches the CRC32 checksum embedded in the last four # bytes; false otherwise. def self.valid?(packet) packet.unpack('C5NN').last == Zlib.crc32(packet[0..8]) end # Public: Unpack a packet for use. # # string packet - Encoded packet to decode # Returns a Packet instance containing the decoded values; or false if the # checksum does not match. def self.unpack(packet) return false unless self.valid?(packet) self.new(*packet.unpack('C5NN')) end # Public: Instance initializer # # integer generation - See class documentation # string ip - See class documentation # integer time - See class documentation def initialize(generation, ip, time) @generation = generation @ip = ip @time = time end # Internal: Transform the generation, IP, time, and checksum into a packed # binary string, according to the format set forth in the class docs. # # Returns a string containing the packed information. def pack a = [ generation ] a.concat(ip.split('.').map(&:to_i) a << time p = a.pack('C5N') a << Zlib.crc32(p) a.pack('C5NN') end alias :to_s :pack end # Public: Instance initializer # # key - OpenSSL::PKey::RSA object representing a 192-bit RSA private key; # used to obfuscate packets prior to their transmission, and to offer # a means of authenticating the knocker during decryption. # tgt_ip - String IP address of the firewall that should receive the knocks. # my_ip - String public IP address of the device knocking; should match the # IP seen by the firewall, to ensure security against replay attacks. def initialize(key, tgt_ip, my_ip) @key = key @tgt_ip = tgt_ip @my_ip = my_ip end # Internal: Encrypt the packet to the provided key # # Returns a string containing the packet encrypted against the key. def encrypted_packet key.private_encrypt(@packet) end # Internal: Checks if the provided port list is acceptable. # # Returns true if the provided list does not contain any ports on the # BAD_PORTS list, false otherwise. def acceptable?(list) BAD_PORTS - list == BAD_PORTS end # Internal: Format a packet into a sequenced series of port numbers suitable for # knocking. # # Returns an array of integers between 0 and 65535 inclusive. def port_list out = [] idx = 0 stack = encrypted_packet.unpack('C*') while a = stack.shift b = stack.shift c = stack.shift one = ( a << 4 ) + ( b & 0xf ) two = ( ( b & 0x0f ) << 8 ) + c out << ( ( one << 4 ) + idx ) idx += 1 out << ( ( two << 4 ) + idx ) idx += 1 end out end # Internal: Same as #port_list, but makes sure the port list is #acceptable? # # Returns an array of integers between 0 and 65535 inclusive, excluding ports # in the BAD_PORTS list. def safe_port_list list = nil until list && acceptable?(list) list = port_list generation += 1 raise Unacceptable if generation > 255 end list end # Public: Perform a knock sequence def knock! sock = UDPSocket.new safe_port_list.each {|port| sock.send('', 0, dst_ip, port) } end end key = OpenSSL::PKey::RSA.new(192) my_ip = '10.0.0.2' tgt_ip = '10.0.0.1' CocKnocker.new(key, tgt_ip, my_ip).knock!
Note: You are viewing an old version of this page. View the current version.