Skip to content

Instantly share code, notes, and snippets.

@stereocat
Last active July 13, 2016 09:29
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save stereocat/6839659 to your computer and use it in GitHub Desktop.
Save stereocat/6839659 to your computer and use it in GitHub Desktop.
simple-router + udp echo server function using Pio
require_relative 'pio-l4hdr'
dst_mac = "54:52:00:01:00:02"
src_mac = "04:20:9a:44:cf:63"
## test data 1
## tcp syn packet, checksum: 0x08e4
## condition: has options.
src_ip = "192.168.1.16"
dst_ip = "192.168.1.1"
src_port = 53920
dst_port = 7
seq_number = 0x6b7884aa
ack_number = 0x0
control_flag = Pio::TcpHeader::CF_SYN
window = 0x2000
urg_pointer = 0x0
options = [
{ :mss => { :segment_size => 1460 } },
:noop,
{ :wsopt => { :shift_count => 8 } },
:noop,
:noop,
:sackp
]
payload = ''
# ## test data2
# ## echo response, checksum: 0x8425
# ## condition: no options
# src_ip = "192.168.1.1"
# dst_ip = "192.168.1.16"
# src_port = 7
# dst_port = 53920
# seq_number = 0xd6ccd4c0
# ack_number = 0x6b7884b0
# control_flag = Pio::TcpHeader::CF_ACK | Pio::TcpHeader::CF_PSH
# window = 0x101b
# urg_pointer = 0x0
# options = []
# payload = %q/abcde/
# ## sample, for options-test of eol/align
# options = [
# { :mss => { :segment_size => 0x0586 } },
# :noop, { :wsopt => { :shift_count => 6 } },
# :noop, :noop, { :sack => {
# :sequence_number_list => [ 0xd8fd48ae, 0xd8fd48af ]
# }},
# :sackp, :eol, :eol # pattern1 (eol at last)
# # :nop, :nop, :sackp # pattern2 (32bit align)
# # :eol, :sackp, :eol # pattern3 (irregular eol)
# ]
############################################################
# topt0 = Pio::TcpOptionalTlv.new(
# :tlv_type => 0
# )
# topt1 = Pio::TcpOptionalTlv.new(
# :tlv_type => 1
# )
# topt2 = Pio::TcpOptionalTlv.new(
# :tlv_type => 2,
# :tlv_body => {
# # :tlv_length => 0x04,
# :segment_size => 0x0586
# }
# )
# topt3 = Pio::TcpOptionalTlv.new(
# :tlv_type => 3,
# :tlv_body => {
# # :tlv_length => 3,
# :shift_count => 0x06
# }
# )
# topt4 = Pio::TcpOptionalTlv.new(
# :tlv_type => 4
# )
# topt5 = Pio::TcpOptionalTlv.new(
# :tlv_type => 5,
# :tlv_body => {
# # :tlv_length => 0x0a,
# :sequence_number_list => [
# 0xd8fd48ae, 0xd8fd48af
# ]
# }
# )
# # print topt5.to_binary_s
# thdr = Pio::TcpHeader.new(
# :src_port => src_port,
# :dst_port => dst_port,
# :seq_number => seq_number,
# :ack_number => ack_number,
# :checksum => 0xefdc, # dummy!
# :urg_pointer => urg_pointer,
# :optional_tlv => [
# topt2,
# topt1, topt3,
# topt1, topt1, topt5,
# # topt4, topt0, topt0, # pattern1 (eol at last)
# # topt1, topt1, topt4, # pattern2 (32bit align)
# topt0, topt4, topt1 # pattern3 (irregular eol)
# ],
# :payload => "abcde"
# )
### tests
# print thdr.to_binary_s
# =>
# stereocat@oftest06:~/training/packetgen$ ruby packetgen_tcp.rb | od -x
# 0000000 cdab 2301 3412 7856 6587 2143 0260 0000
# 0000020 dcef 0000 0402 8605 0301 0603 0101 0a05
# 0000040 fdd8 ae48 fdd8 af48 0204 0000 6261 6463
# 0000060 0065
# 0000061
# stereocat@oftest06:~/training/packetgen$
# print thdr.optional_tlv.to_binary_s
# =>
# stereocat@oftest06:~/training/packetgen$ ruby packetgen_tcp.rb | od -x
# 0000000 0402 8605 0301 0603 0101 0a05 fdd8 ae48
# 0000020 fdd8 af48 0400 0102
# 0000030
# stereocat@oftest06:~/training/packetgen$
# options: 24bytes
# thdr.optional_tlv.to_binary_s.each_byte do | each |
# puts "byte : #{ sprintf("%02x", each) }"
# end
# =>
# stereocat@oftest06:~/training/packetgen$ ruby packetgen_tcp.rb
# byte : 02
# byte : 04
# byte : 05
# byte : 86
# byte : 01
# byte : 03
# byte : 03
# byte : 06
# byte : 01
# byte : 01
# byte : 05
# byte : 0a
# byte : d8
# byte : fd
# byte : 48
# byte : ae
# byte : d8
# byte : fd
# byte : 48
# byte : af
# byte : 00
# byte : 04
# byte : 02
# byte : 01
# stereocat@oftest06:~/training/packetgen$
############################################################
tdgm = Pio::TcpDatagram.new(
:src_ip => src_ip,
:dst_ip => dst_ip,
:src_port => src_port,
:dst_port => dst_port,
:seq_number => seq_number,
:ack_number => ack_number,
:control_flag => control_flag,
:window => window,
:urg_pointer => urg_pointer,
:optional_tlv => options,
:payload => payload,
)
# print tdgm.to_binary
############################################################
ippct = Pio::IPv4Packet.new(
:payload => tdgm
)
ipfrm = Pio::IPv4Frame.new(
:src_mac => src_mac,
:dst_mac => dst_mac,
:packet => ippct
)
print ipfrm.to_binary
require_relative "pio-l4hdr"
dst_mac = "54:52:00:01:00:02"
src_mac = "04:20:9a:44:cf:63"
src_ip = "192.168.1.83"
dst_ip = "192.168.1.103"
src_port = 7
dst_port = 64942
payload = %q/abcde/
udgm = Pio::UdpDatagram.new(
:src_ip => src_ip,
:dst_ip => dst_ip,
:src_port => src_port,
:dst_port => dst_port,
:payload => payload
)
#print udgm.to_binary
ippct = Pio::IPv4Packet.new(
:payload => udgm
)
#print ippct.to_binary
ipfrm = Pio::IPv4Frame.new(
:src_mac => src_mac,
:dst_mac => dst_mac,
:packet => ippct
)
print ipfrm.to_binary
# -*- coding: utf-8 -*-
require_relative "pio-l4hdr"
ipfrm = Pio::IPv4Frame.read $stdin
puts "* ipv4 frame"
puts " src mac : #{ ipfrm.src_mac }"
puts " dst mac : #{ ipfrm.dst_mac }"
puts " ether type : #{ sprintf("%04x", ipfrm.ether_type) }"
puts "* ipv4 packet"
ippct = ipfrm.packet
puts " version : #{ ippct.version }"
puts " header length: #{ ippct.header_length * 4 } (octets)"
puts " tos : #{ ippct.tos }"
puts " total length : #{ ippct.total_length } (octets)"
puts " identificat : #{ ippct.identification }"
puts " flags : #{ ippct.flags }"
puts " frag offset : #{ ippct.fragment_offset }"
puts " ttl : #{ ippct.ttl }"
puts " protocol : #{ ippct.protocol }"
puts " checksum : #{ sprintf("%04x", ippct.header_checksum) }"
puts " src addr : #{ ippct.src_ip }"
puts " dst addr : #{ ippct.dst_ip }"
puts " options : #{ ippct.options }"
puts " valid? : #{ ippct.valid? }"
puts "* ipv4 payload (proto:#{ippct.protocol} datagram)"
dgm = ippct.datagram
puts " src port : #{ dgm.src_port }"
puts " dst port : #{ dgm.dst_port }"
puts " seq number : #{ sprintf("%08x", dgm.seq_number) }"
puts " ack number : #{ sprintf("%08x", dgm.ack_number) }"
puts " data offset : #{ dgm.data_offset }"
puts " ctrl flag : #{ sprintf("%06b", dgm.control_flag) }"
puts " window size : #{ sprintf("%04x", dgm.window) }"
puts " checksum : #{ sprintf("%04x", dgm.checksum) }"
puts " urg pointer : #{ sprintf("%04x", dgm.urg_pointer) }"
puts " options : #{ (dgm.optional_tlv.map{ |e| e.tlv_type }).join(", ") }"
# puts " options : #{ dgm.optional_tlv }"
# if dgm.optional_tlv
# dgm.optional_tlv.each do | each |
# puts " - type : #{ each.tlv_type }"
# end
# end
puts " payload : #{ dgm.payload }"
puts " valid? : #{ dgm.valid? }"
############################################################
# hdr = Pio::TcpHeader.read $stdin
# puts "* tcp datagram"
# puts " src port : #{ hdr.src_port }"
# puts " dst port : #{ hdr.dst_port }"
# puts " seq number : #{ sprintf("%08x", hdr.seq_number) }"
# puts " ack number : #{ sprintf("%08x", hdr.ack_number) }"
# puts " data offset : #{ sprintf("%x", hdr.data_offset) }"
# puts " ctrl flag : #{ sprintf("%x", hdr.control_flag) }"
# puts " window size : #{ sprintf("%04x", hdr.window) }"
# puts " checksum : #{ sprintf("%04x", hdr.checksum) }"
# puts " urg pointer : #{ sprintf("%04x", hdr.urg_pointer) }"
# puts " options : #{ hdr.optional_tlv }"
# if hdr.optional_tlv
# hdr.optional_tlv.each do | each |
# puts " - type : #{ each.tlv_type }"
# end
# end
# puts " payload : #{ hdr.payload }"
# src_ip = "192.168.1.16"
# dst_ip = "192.168.1.1"
# dgm = Pio::TcpDatagram.new(
# :src_ip => src_ip,
# :dst_ip => dst_ip,
# :datagram => hdr,
# )
# puts " valid? : #{ dgm.valid? }"
############################################################
# TODO
# OK?
# - oiptional_tlv ナシのデータをよむとエラー
# => onlyif で data_offset == 5 のときは array いれない。
# - payload がよめてない。-> count_bytes_remaining がよくわからん。動い
# てる気配がない。
# => 'rest' keyword でのこり全部よむ
# - option が 32bit align only (eolでおわらない)場合にエラー
# - option 中の変なところに eol がはいった場合はどうなる? (よみとばしできる?)
# => 上ふたつは option 読んだ分のカウント + skip で対応
# $LOAD_PATH.unshift File.expand_path( File.join File.dirname( __FILE__ ), "lib" )
require_relative "pio-l4hdr"
# stereocat@oftest06:~/training/packetgen$ ruby pgdummy.rb | od -x
# 0000000 0000 0100 0200 1800 c1b9 c34f 0008 0045
# 0000020 2400 712a 0040 113f a48c a8c0 1002 a8c0
# 0000040 5301 f285 0700 1000 98a5 6a6d 5c33 5f5a
# 0000060 6254 0000 0000 0000 0000 0000
# 0000074
# stereocat@oftest06:~/training/packetgen$
# str = ["0000000100020018b9c14fc30800450000242a7140003f118ca4c0a80210c0a8015385f200070010a5986d6a335c5a5f546200000000000000000000"].pack("H*")
# ipfrm = Pio::IPv4Frame.read str
ipfrm = Pio::IPv4Frame.read $stdin
puts "* ipv4 frame"
puts " src mac : #{ ipfrm.src_mac }"
puts " dst mac : #{ ipfrm.dst_mac }"
puts " ether type : #{ sprintf("%04x", ipfrm.ether_type) }"
puts "* ipv4 packet"
# ippct = Pio::IPv4Packet.read $stdin
ippct = ipfrm.packet
puts " version : #{ ippct.version }"
puts " header length: #{ ippct.header_length * 4 } (octets)"
puts " tos : #{ ippct.tos }"
puts " total length : #{ ippct.total_length } (octets)"
puts " identificat : #{ ippct.identification }"
puts " flags : #{ ippct.flags }"
puts " frag offset : #{ ippct.fragment_offset }"
puts " ttl : #{ ippct.ttl }"
puts " protocol : #{ ippct.protocol }"
puts " checksum : #{ sprintf("%04x", ippct.header_checksum) }"
puts " src addr : #{ ippct.src_ip }"
puts " dst addr : #{ ippct.dst_ip }"
puts " options : #{ ippct.options }"
puts " valid? : #{ ippct.valid? }"
puts "* ipv4 payload (proto:#{ippct.protocol} datagram)"
dgm = ippct.datagram
puts " src port : #{ dgm.src_port }"
puts " dst port : #{ dgm.dst_port }"
puts " total length : #{ dgm.total_length } (octets)"
puts " checksum : #{ sprintf("%04x", dgm.checksum) }"
puts " payload : #{ dgm.payload }"
puts " valid? : #{ dgm.valid? }"
# -*- coding: utf-8 -*-
require "forwardable"
require 'rubygems'
require 'bindata'
require 'pio'
module Pio
module HeaderUtil
def get_checksum csum, val
# $stderr.puts("val : #{sprintf("%06x, %04x", csum, val)}")
sum = ( ~csum & 0xffff ) + val
while sum > 0xffff
sum = ( sum & 0xffff ) + ( sum >> 16 )
end
~sum & 0xffff
end
def get_str_checksum csum, str
dbyte = 0
str.each_byte do | byte |
if dbyte == 0
dbyte = byte << 8
else
dbyte = dbyte + byte
csum = get_checksum( csum, dbyte )
dbyte = 0
end
end
# padding (align 16bit)
csum = get_checksum( csum, dbyte ) if dbyte != 0
return csum
end
end
############################################################
## IPv4
############################################################
class IPv4FrameHeader < BinData::Record
extend Type::EthernetHeader
endian :big
ethernet_header :ether_type => 0x0800
end
class IPv4Frame
extend Forwardable
attr_reader :src_mac
attr_reader :dst_mac
attr_reader :frame_hdr
attr_reader :packet
def_delegators :@frame_hdr, :ether_type
# minimum frame length = 60 octets
MIN_FRAME_LEN = 60
# ethernet header length = 14 octets
# [ mac:6octets * 2 + ether-type 2octets ]
ETH_HEADER_LEN = 14
# minimum packet length (octets)
# packet must be larger than 46 octets = 60 - 14
MIN_PACKET_LEN = MIN_FRAME_LEN - ETH_HEADER_LEN
def initialize opts
if opts[ :frame_header ]
@frame_hdr = opts[ :frame_header ] # IPv4FrameHeader
@packet = opts[ :packet ] # IPv4Packet
@src_mac = Mac.new( @frame_hdr.source_mac.to_s )
@dst_mac = Mac.new( @frame_hdr.destination_mac.to_s )
else
@src_mac = Mac.new( opts[ :src_mac ] )
@dst_mac = Mac.new( opts[ :dst_mac ] )
@packet = opts[ :packet ] # IPv4Packet
@frame_hdr = IPv4FrameHeader.new(
:source_mac => @src_mac.to_ary,
:destination_mac => @dst_mac.to_ary
)
end
end
def to_binary
padcount = 0
if @packet.total_length < MIN_PACKET_LEN
padcount = MIN_PACKET_LEN - @packet.total_length
end
@frame_hdr.to_binary_s + @packet.to_binary + ( "\000" * padcount )
end
def self.read io
io = StringIO.new(io, 'r') if String === io
# 分割して read するので IO class compatible にする。
frame = IPv4FrameHeader.read io
ippct = IPv4Packet.read io
IPv4Frame.new(
:frame_header => frame,
:packet => ippct
)
end
end
class IPv4Header < BinData::Record
endian :big
bit4 :version, :value => 4
bit4 :header_length, :initial_value => 5 # without options
uint8 :tos, :initial_value => 0
uint16 :total_length, :value => lambda {
payload.bytesize + header_length_in_bytes
}
uint16 :identification
bit3 :flags, :initial_value => 2 # 2:don't fragment
bit13 :fragment_offset, :initial_value => 0
uint8 :ttl, :initial_value => 64
uint8 :protocol
uint16 :header_checksum, :initial_value => 0
ip_address :src_ip
ip_address :dst_ip
string :options, :read_length => lambda {
# length of ipv4 header without options is 20 octets
header_length_in_bytes - 20
}
string :payload, :read_length => lambda {
total_length - header_length_in_bytes
}
def header_length_in_bytes
# IHL(internet header length) is 4 octets count
header_length * 4
end
end
class IPv4Packet
extend Forwardable
include HeaderUtil
attr_reader :packet
attr_reader :src_ip
attr_reader :dst_ip
def_delegator :@packet, :to_binary_s, :to_binary
def_delegators :@packet, :version
def_delegators :@packet, :header_length
def_delegators :@packet, :tos
def_delegators :@packet, :total_length
def_delegators :@packet, :identification
def_delegators :@packet, :flags
def_delegators :@packet, :fragment_offset
def_delegators :@packet, :ttl
def_delegators :@packet, :protocol
def_delegators :@packet, :header_checksum
def_delegators :@packet, :src_ip
def_delegators :@packet, :dst_ip
def_delegators :@packet, :options
def_delegators :@packet, :payload
@@id = nil
def id
unless @@id
srand Random.new_seed
@@id = Random.rand(1..65535)
else
@@id = (@@id + 1) & 0xffff
end
end
def initialize opts
if opts[ :packet ]
@packet = opts[ :packet ]
@src_ip = @packet.src_ip
@dst_ip = @packet.dst_ip
@protocol = @packet.protocol
@payload = @packet.payload
else
if opts[ :payload ]
@payload = opts[ :payload ]
case @payload
when TransportLayerProtocol
@src_ip = @payload.src_ip
@dst_ip = @payload.dst_ip
@protocol = @payload.protocol
else
raise ArgmentError, "## TBD ##"
end
@packet = IPv4Header.new(
:src_ip => @src_ip.to_ary,
:dst_ip => @dst_ip.to_ary,
:protocol => @protocol,
:identification => id,
:payload => @payload.to_binary
)
@packet.header_checksum = header_checksum
else
raise ArgmentError
end
end
end
def self.read io
iphdr = IPv4Header.read io
IPv4Packet.new( :packet => iphdr )
end
def datagram
case @protocol
when 6 # tcp
thdr = TcpHeader.read( @payload )
TcpDatagram.new(
:src_ip => @src_ip.to_s,
:dst_ip => @dst_ip.to_s,
:datagram => thdr
)
when 17 # udp
uhdr = UdpHeader.read( @payload )
UdpDatagram.new(
:src_ip => @src_ip.to_s,
:dst_ip => @dst_ip.to_s,
:datagram => uhdr
)
end
end
def valid?
csum = header_checksum
csum = get_checksum( csum, @packet.header_checksum )
return csum == 0 ? true : false
end
def header_checksum
csum = 0
csum = get_checksum( csum,
@packet.version << 12 | @packet.header_length << 8 | @packet.tos )
csum = get_checksum( csum, @packet.total_length )
csum = get_checksum( csum, @packet.identification )
csum = get_checksum( csum,
@packet.flags << 13 + @packet.fragment_offset )
csum = get_checksum( csum,
@packet.ttl << 8 | @packet.protocol )
csum = get_checksum( csum, @src_ip.to_i >> 16 )
csum = get_checksum( csum, @src_ip.to_i & 0xffff )
csum = get_checksum( csum, @dst_ip.to_i >> 16 )
csum = get_checksum( csum, @dst_ip.to_i & 0xffff )
# cannot handle ip options
return csum
end
end
############################################################
## L4 Protocol Base
############################################################
class TransportLayerProtocol
extend Forwardable
include HeaderUtil
attr_reader :datagram # Bindata::Record Struct
attr_reader :src_ip
attr_reader :dst_ip
# attr_reader :src_port
# attr_reader :dst_port
attr_reader :total_length
# attr_reader :payload
def_delegator :@datagram, :to_binary_s, :to_binary
def_delegators :@datagram, :src_port
def_delegators :@datagram, :dst_port
def_delegators :@datagram, :checksum
def_delegators :@datagram, :payload
# abstract
def protocol; 0; end
# abstract
def header_checksum; 0; end
def valid?
csum = header_checksum
csum = get_checksum( csum, @datagram.checksum )
return csum == 0 ? true : false
end
end
############################################################
## TCP
############################################################
class TcpOption < BinData::Record
endian :big
end
# class EndOfOptionList < TcpOption
# # tlv_type only
# end
# class NoOperation < TcpOption
# # tlv_type only
# end
class MaximumSegmentSize < TcpOption
# type: 2
bit8 :tlv_length, :value => 4
bit16 :segment_size
end
class WindowScaleOption < TcpOption
# type: 3
bit8 :tlv_length, :value => 3
bit8 :shift_count
end
class SackPermitted < TcpOption
# type: 4
bit8 :tlv_length, :value => 2
# tlv_type and length only
end
class Sack < TcpOption
# type: 5
bit8 :tlv_length, :value => lambda {
sequence_number_list.length * 4 + 2
}
array :sequence_number_list,
:type => :bit32,
:initial_length => lambda { ( tlv_length - 2 ) / 4 }
end
class TimeStampOption < TcpOption
# type: 8
bit8 :tlv_length, :value => 10
bit32 :ts_val # timestamp vlaue
bit32 :ts_ecr # timestamp reply
end
class PartialOrderConnectionPermitted < TcpOption
# time: 9
bit8 :tlv_length, :value => 2
end
class PartialOrderServiceProfile < TcpOption
# type: 10
bit8 :tlv_length, :value => 3
bit1 :start_flag
bit1 :end_flag
bit6 :filler
end
class Cc < TcpOption
# type: 11
bit8 :tlv_length, :value => 6
bit32 :connection_count
end
class CcNew < TcpOption
# type: 12
bit8 :tlv_length, :value => 6
bit32 :connection_count
end
class CcEcho < TcpOption
# type: 13
bit8 :tlv_length, :value => 6
bit32 :connection_count
end
class TcpAlternateChecksumRequest < TcpOption
# type: 14
bit8 :tlv_length, :value => 3
bit8 :checksum
end
class TcpAlternateChecksumData < TcpOption
# type: 15
bit8 :tlv_length, :value => lambda {
data.bytesize + 2
}
string :data
end
class TcpOptionalTlv < BinData::Record
endian :big
bit8 :tlv_type
choice :tlv_body,
:onlyif => lambda { not type_only? },
:selection => :chooser do
# end_of_option_list 0
# no_operation 1
maximum_segment_size 2
window_scale_option 3
sack_permitted 4
sack 5
time_stamp_option 8
partial_order_connection_permitted 9
partial_order_service_profile 10
cc 11
cc_new 12
cc_echo 13
tcp_alternate_checksum_request 14
tcp_alternate_checksum_data 15
string "unknown"
end
def chooser
case tlv_type
when 2,3,4,5,8,9,10,11,12,13,14,15
# $stderr.puts "## type = #{tlv_type}"
tlv_type
else
"unknown"
end
end
def end_of_option_list?
tlv_type == 0
end
def type_only?
tlv_type == 0 or tlv_type == 1
end
def bytesize
case tlv_type
when 0, 1
1
when 2,3,4,5,8,9,10,11,12,13,14,15
tlv_body.tlv_length
else
0
end
end
end
class TcpHeader < BinData::Record
endian :big
CF_FIN = 0b000001
CF_SYN = 0b000010
CF_RST = 0b000100
CF_PSH = 0b001000
CF_ACK = 0b010000
CF_URG = 0b100000
uint16 :src_port
uint16 :dst_port
uint32 :seq_number
uint32 :ack_number, :initial_value => 0
bit4 :data_offset, :initial_value => lambda {
# $stderr.puts "# A optlen: #{ option_length_in_bytes }"
5 + option_length_in_bytes / 4
}
bit6 :reserved, :value => 0
bit6 :control_flag, :initial_value => CF_SYN
uint16 :window
uint16 :checksum
uint16 :urg_pointer, :initial_value => 0
array :optional_tlv,
:type => :tcp_optional_tlv,
:onlyif => :has_options?,
:read_until => lambda {
@readbytes ||= header_length_in_bytes - 20
@readbytes = @readbytes - element.bytesize
# $stderr.puts "# B optlen: #{ element.tlv_type} => #{ option_length_in_bytes }, #{ @readbytes }"
element.end_of_option_list? or @readbytes <= 0
}
skip :length => lambda { @readbytes or 0 }
rest :payload
def has_options?
data_offset > 5
end
def option_length_in_bytes
bytes = 0
if optional_tlv
optional_tlv.each do | each |
bytes += each.bytesize
end
end
return bytes
end
def header_length_in_bytes
# data_offset = tcp header length, 4 octets count
data_offset * 4
end
def fin? ; ( control_flag & CF_FIN ) > 0 ; end
def syn? ; ( control_flag & CF_SYN ) > 0 ; end
def rst? ; ( control_flag & CF_RST ) > 0 ; end
def psh? ; ( control_flag & CF_PSH ) > 0 ; end
def ack? ; ( control_flag & CF_ACK ) > 0 ; end
def urg? ; ( control_flag & CF_URG ) > 0 ; end
def fin_ack? ; ( fin? and ack? ) ; end
def syn_ack? ; ( syn? and ack? ) ; end
def psh_ack? ; ( psh? and ack? ) ; end
end
class TcpDatagram < TransportLayerProtocol
def_delegators :@datagram, :seq_number
def_delegators :@datagram, :ack_number
def_delegators :@datagram, :data_offset
def_delegators :@datagram, :control_flag
def_delegators :@datagram, :window
def_delegators :@datagram, :urg_pointer
def_delegators :@datagram, :optional_tlv
def_delegators :@datagram, :fin?, :syn?, :rst?, :psh?, :ack?, :urg?
def_delegators :@datagram, :fin_ack?, :syn_ack?, :psh_ack?
def protocol; 6; end
def initialize opts
@src_ip = IPv4Address.new( opts[ :src_ip ] )
@dst_ip = IPv4Address.new( opts[ :dst_ip ] )
if opts[ :datagram ]
@datagram = opts[ :datagram ]
@src_port = @datagram.src_port
@dst_port = @datagram.dst_port
@payload = @datagram.payload
@seq_number = @datagram.seq_number
@ack_number = @datagram.ack_number
@control_flag = @datagram.control_flag
@window = @datagram.window
@urg_pointer = @datagram.urg_pointer
# array of TcpOptionalTlv
@options = @datagram.optional_tlv
@total_length = @datagram.to_binary_s.bytesize
@header_length = @datagram.header_length_in_bytes
# $stderr.puts "#[B] hdrlen=#{@header_length}, tlen=#{@total_length}"
# TBD
# ハンパなheaderわたされることを想定して
# 各パラメタの再計算をするか?
else
@src_port = opts[ :src_port ]
@dst_port = opts[ :dst_port ]
@payload = opts[ :payload ]
@seq_number = opts[ :seq_number ]
@ack_number = opts[ :ack_number ]
@control_flag = opts[ :control_flag ] || Pio::TcpHeader::CF_SYN
@window = opts[ :window ]
@urg_pointer = opts[ :urg_pointer ] || 0
# array of TcpOptionalTlv
@options = options_by( opts[ :optional_tlv ] )
# TBD
# need total_length and header_length (data_offset)
# to calculate checksum!
# Optionsの処理を先にどうにかしたほうがいいか...
# header = fixed fields (20 bytes) + option fields length
@header_length = 20 + option_length_in_bytes
@total_length = @header_length + @payload.bytesize
# $stderr.puts "#[A] hdrlen=#{@header_length}, tlen=#{@total_length}"
@datagram = TcpHeader.new(
:src_port => @src_port,
:dst_port => @dst_port,
:seq_number => @seq_number,
:ack_number => @ack_number,
:control_flag => @control_flag,
:window => @window,
:urg_pointer => @urg_pointer,
:checksum => header_checksum,
:optional_tlv => @options,
:payload => @payload
)
end
end
def header_checksum
csum = 0
# pseudo header
csum = get_checksum( csum, @src_ip.to_i >> 16 )
csum = get_checksum( csum, @src_ip.to_i & 0xffff )
csum = get_checksum( csum, @dst_ip.to_i >> 16 )
csum = get_checksum( csum, @dst_ip.to_i & 0xffff )
csum = get_checksum( csum, protocol & 0x00ff )
csum = get_checksum( csum, @total_length )
# tcp header (without checksum)
csum = get_checksum( csum, @src_port )
csum = get_checksum( csum, @dst_port )
csum = get_checksum( csum, @seq_number >> 16 )
csum = get_checksum( csum, @seq_number & 0xffff )
csum = get_checksum( csum, @ack_number >> 16 )
csum = get_checksum( csum, @ack_number & 0xffff )
# $stderr.puts "hdrlen=#{@header_length}, flag=#{@control_flag}, value=#{sprintf("%04x", (( @header_length/4 << 12 ) & 0xf000 | @control_flag))}"
csum = get_checksum( csum,
( @header_length/4 << 12 ) & 0xf000 | @control_flag
)
csum = get_checksum( csum, @window )
csum = get_checksum( csum, @urg_pointer )
# tcp options
csum = get_str_checksum( csum, @options.map{ |e| e.to_binary_s }.join )
# payload
csum = get_str_checksum( csum, @payload.to_s )
return csum
end
def options_by opt_words
return [] unless opt_words
# opt_words format:
# [
# :tlv_type_symbol,
# { :tlv_type_symbol => {
# :arg_symbol => value,
# :arg_symbol => value,
# ...
# }},
# ...
# ]
opts = []
opt_words.each do | each |
case each
when Symbol
if type = option_type_table[ each ]
opts.push Pio::TcpOptionalTlv.new(
:tlv_type => type
)
else
$stderr.puts "warning: unknown option: #{each}"
end
when Hash
( type_sym, arg ) = each.to_a.shift
if type = option_type_table[ type_sym ]
opts.push Pio::TcpOptionalTlv.new(
:tlv_type => type,
:tlv_body => arg
)
else
$stderr.puts "warning: unknown option: #{type_sym}"
end
end
end
return opts
end
def option_type_table
{
# short
:eol => 0,
:noop => 1,
:mss => 2,
:wsopt => 3,
:sackp => 4,
:tsopt => 8,
:pocp => 9,
:posp => 10,
:tcpacr => 14,
:tcpacd => 15,
# long
:end_of_option_list => 0,
:no_operation => 1,
:maximum_segment_size => 2,
:window_scale_option => 3,
:sack_permitted => 4,
:sack => 5,
:time_stamp_option => 8,
:partial_order_connection_permitted => 9,
:partial_order_service_profile => 10,
:cc => 11,
:cc_new => 12,
:cc_echo => 13,
:tcp_alternate_checksum_request => 14,
:tcp_alternate_checksum_data => 15
}
end
def option_length_in_bytes
bytes = 0
if @options
@options.each do | each |
bytes = bytes + each.bytesize
end
end
return bytes
end
end
class TcpServer
attr_reader :curr_state
def initialize
@curr_state = :closed
srand Random.new_seed
@seq = Random.rand(1..0xffffffff)
@handler = {}
end
def send_handler &block
@handler[ :send ] = block
end
def request_handler &block
@handler[ :request ] = block
end
def response_datagram req_dgm, ack, cflag, payload
TcpDatagram.new(
:src_ip => req_dgm.dst_ip.to_s,
:dst_ip => req_dgm.src_ip.to_s,
:src_port => req_dgm.dst_port,
:dst_port => req_dgm.src_port,
:seq_number => @seq,
:ack_number => ack,
:window => 2048, # ok?
:control_flag => cflag,
:optional_tlv => [],
:payload => payload
)
end
def calc_ack rcv_dgm, count
@seq = rcv_dgm.ack_number unless rcv_dgm.ack_number == 0
( rcv_dgm.seq_number + count ) & 0xffffffff
end
def receive rcv_dgm
case @curr_state
when :closed, :listen
if rcv_dgm.syn?
ack = calc_ack( rcv_dgm, 1 )
cflag = TcpHeader::CF_SYN | TcpHeader::CF_ACK
reply_dgm = response_datagram( rcv_dgm, ack, cflag, '' )
## send syn_ack
@handler[ :send ].call( reply_dgm )
@curr_state = :syn_received
end
when :syn_received
if rcv_dgm.ack?
# finish handshake. wait request
@curr_state = :established
end
when :established
if rcv_dgm.fin?
ack = calc_ack( rcv_dgm, 1 )
cflag = TcpHeader::CF_ACK
reply_dgm = response_datagram( rcv_dgm, ack, cflag, '' )
## send ack
@handler[ :send ].call( reply_dgm )
@curr_state = :close_wait
cflag = TcpHeader::CF_FIN
reply_dgm = response_datagram( rcv_dgm, ack, cflag, '' )
## send fin
@handler[ :send ].call( reply_dgm )
@curr_state = :last_ack
elsif
## request processing
ack = calc_ack( rcv_dgm, rcv_dgm.payload.bytesize )
reply_dgm = @handler[ :request ].call( rcv_dgm, @seq, ack )
## send response
@handler[ :send ].call( reply_dgm )
end
when :last_ack
if rcv_dgm.ack?
@curr_state = :closed
puts "info, session closed."
end
else
$stderr.puts "warning, receive out-of-state packet"
end
end
end
############################################################
## UDP
############################################################
class UdpHeader < BinData::Record
endian :big
UDP_HDR_LEN = 8
uint16 :src_port
uint16 :dst_port
uint16 :total_length, :value => lambda {
payload.bytesize + UDP_HDR_LEN
}
uint16 :checksum
string :payload, :read_length => lambda {
total_length - UDP_HDR_LEN
}
end
class UdpDatagram < TransportLayerProtocol
def protocol; 17; end
def initialize opts
@src_ip = IPv4Address.new( opts[ :src_ip ] )
@dst_ip = IPv4Address.new( opts[ :dst_ip ] )
if opts[ :datagram ]
@datagram = opts[ :datagram ]
@src_port = @datagram.src_port
@dst_port = @datagram.dst_port
@total_length = @datagram.total_length
@payload = @datagram.payload
else
@src_port = opts[ :src_port ]
@dst_port = opts[ :dst_port ]
@payload = opts[ :payload ]
# udp datagram length
# = payload length (octets) + udp header length (8 octets)
@total_length = @payload.to_s.bytesize + 8
@datagram = UdpHeader.new(
:src_port => @src_port,
:dst_port => @dst_port,
:total_length => @total_length,
:checksum => header_checksum,
:payload => @payload
)
end
end
def header_checksum
csum = 0
# pseudo header
csum = get_checksum( csum, @src_ip.to_i >> 16 )
csum = get_checksum( csum, @src_ip.to_i & 0xffff )
csum = get_checksum( csum, @dst_ip.to_i >> 16 )
csum = get_checksum( csum, @dst_ip.to_i & 0xffff )
csum = get_checksum( csum, protocol & 0x00ff )
csum = get_checksum( csum, @total_length )
# udp header (without checksum)
csum = get_checksum( csum, @src_port )
csum = get_checksum( csum, @dst_port )
csum = get_checksum( csum, @total_length )
# udp payload
csum = get_str_checksum( csum, @payload.to_s )
return csum
end
end
end
# -*- coding: utf-8 -*-
#
# A router implementation in Trema
#
# Copyright (C) 2013 NEC Corporation
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
require "arp-table"
require "interface"
require "router-utils"
require "routing-table"
require "pio"
require_relative "pio-l4hdr"
class SimpleRouter < Controller
include RouterUtils
# add_timer_event :send_echo_request_periodically, 5, :periodic
def start
info "[start]"
# load "simple_router.conf"
load "echoserver.conf"
@interfaces = Interfaces.new( $interface )
@arp_table = ARPTable.new
@routing_table = RoutingTable.new( $route )
@tcp_sessions = {}
end
def switch_ready dpid
info "[switch_ready] #{ dpid.to_hex }"
send_message dpid, FeaturesRequest.new
# echo server settings
@srv_dpid = dpid
@srv_edst = Pio::IPv4Address.new( "192.168.1.16" ) # echo dst
srv_addr = Pio::IPv4Address.new( "192.168.1.83" )
@srv_int = @interfaces.find_by_ipaddr( srv_addr )
unless @srv_int
error "server #{srv_addr.to_s} : interface not found"
exit
end
@ready = true
end
def features_reply(dpid, message)
info "[features_reply] Datapath ID: #{ dpid.to_hex }"
message.ports.each do |each|
puts "Port no: #{ each.number }"
puts " Hardware address: #{ each.hw_addr.to_s }"
puts " Port name: #{ each.name }"
end
end
def send_udp_echo_request_periodically
if @ready
puts "** current arp table **"
@arp_table.dump
arp_entry = @arp_table.lookup( @srv_edst )
if arp_entry
send_udp_echo_request(
@srv_dpid,
@srv_int.hwaddr, arp_entry.hwaddr,
@srv_int.ipaddr, @srv_edst,
32697,
"1234567890abcdefGHIJK",
@srv_int.port
)
else
arp_request = Pio::Arp::Request.new(
:source_mac => @srv_int.hwaddr.to_s,
:target_protocol_address => @srv_edst.to_s,
:sender_protocol_address => @srv_int.ipaddr.to_s
)
packet_out @srv_dpid, arp_request.to_binary, SendOutPort.new( @srv_int.port )
end
else
warn "DPID:#{@dpid.to_hex} is not ready"
end
end
def packet_in( dpid, message )
return if not to_me?( message )
if message.arp_request?
handle_arp_request dpid, message
elsif message.arp_reply?
handle_arp_reply message
elsif message.ipv4?
handle_ipv4 dpid, message
else
# noop.
end
end
private
def to_me?( message )
return true if message.macda.broadcast?
interface = @interfaces.find_by_port( message.in_port )
if interface and interface.has?( message.macda )
return true
end
end
def handle_arp_request( dpid, message )
info "[handle_arp_request] #{dpid.to_hex}"
port = message.in_port
daddr = message.arp_tpa
interface = @interfaces.find_by_port_and_ipaddr( port, daddr )
if interface
arp_reply = Pio::Arp::Reply.new(
:source_mac => interface.hwaddr.to_s,
:destination_mac => message.arp_sha.to_s,
:sender_protocol_address => message.arp_tpa.to_s,
:target_protocol_address => message.arp_spa.to_s
)
packet_out dpid, arp_reply.to_binary, SendOutPort.new( interface.port )
end
end
def handle_arp_reply( message )
info "[handle_arp_reply]"
@arp_table.update message.in_port, message.arp_spa, message.arp_sha
end
def send_udp_echo_request dpid, src_mac, dst_mac, src_ip, dst_ip, src_port, payload, outport
info "[send_udp_echo_request]"
frame = make_udp_echo_frame(
src_mac, dst_mac,
src_ip, dst_ip,
src_port, 7,
payload
)
packet_out( dpid, frame, SendOutPort.new( outport ) )
end
def send_udp_echo_reply dpid, message, data
info "[send_udp_echo_reply]"
frame = make_udp_echo_frame(
message.macda, message.macsa,
message.ipv4_daddr, message.ipv4_saddr,
message.udp_dst_port, message.udp_src_port,
data
)
packet_out( dpid, frame, SendOutPort.new( message.in_port ) )
end
def make_udp_echo_frame( src_mac, dst_mac, src_ip, dst_ip, src_port, dst_port, payload )
puts "[make_udp_echo_frame]"
puts "src mac : #{ src_mac.to_s }"
puts "dst mac : #{ dst_mac.to_s }"
puts "src addr : #{ src_ip.to_s }"
puts "dst addr : #{ dst_ip.to_s }"
puts "src port : #{ src_port }"
puts "dst port : #{ dst_port }"
puts "payload : #{ payload }"
udgm = Pio::UdpDatagram.new(
:src_ip => src_ip.to_s,
:dst_ip => dst_ip.to_s,
:src_port => src_port,
:dst_port => dst_port,
:payload => payload
)
ipfrm = make_ip_frame( src_mac, dst_mac, udgm )
return ipfrm.to_binary
end
def send_tcp_reply dpid, message, tdgm
info "[send_tcp_reply]"
frame = make_ip_frame(
message.macda, message.macsa, tdgm
)
packet_out( dpid,
frame.to_binary,
SendOutPort.new( message.in_port )
)
end
def make_tcp_echo_datagram rcv_dgm, seq, ack
info "[make_tcp_echo_datagram]"
cflag = Pio::TcpHeader::CF_PSH | Pio::TcpHeader::CF_ACK
Pio::TcpDatagram.new(
:src_ip => rcv_dgm.dst_ip.to_s,
:dst_ip => rcv_dgm.src_ip.to_s,
:src_port => rcv_dgm.dst_port,
:dst_port => rcv_dgm.src_port,
:seq_number => seq,
:ack_number => ack,
:control_flag => cflag,
:window => 2048, # ok?
:payload => rcv_dgm.payload # echo back
)
end
def make_ip_frame( src_mac, dst_mac, dgm )
info "[make_ip_frame]"
ippct = Pio::IPv4Packet.new(
:payload => dgm
)
ipfrm = Pio::IPv4Frame.new(
:src_mac => src_mac.to_s,
:dst_mac => dst_mac.to_s,
:packet => ippct
)
return ipfrm
end
def handle_ipv4( dpid, message )
info "[handle_ipv4], #{dpid.to_hex}"
if should_forward?( message )
forward dpid, message
elsif message.icmpv4_echo_request?
handle_icmpv4_echo_request dpid, message
else
begin
ipfrm = Pio::IPv4Frame.read message.data
rescue
warn "[handle_ipv4] can not handle packet: #{message.ipv4_saddr.to_s}->#{message.ipv4_daddr.to_s}"
puts "data -> #{message.data.class}, #{message.data.unpack('H*')}"
return
end
puts " src mac : #{ ipfrm.src_mac }"
puts " dst mac : #{ ipfrm.dst_mac }"
puts " ether type : #{ sprintf("%04x", ipfrm.ether_type) }"
ippct = ipfrm.packet
puts " version : #{ ippct.version }"
puts " header length: #{ ippct.header_length * 4 } (octets)"
puts " tos : #{ ippct.tos }"
puts " total length : #{ ippct.total_length } (octets)"
puts " identificat : #{ ippct.identification }"
puts " flags : #{ ippct.flags }"
puts " frag offset : #{ ippct.fragment_offset }"
puts " ttl : #{ ippct.ttl }"
puts " protocol : #{ ippct.protocol }"
puts " checksum : #{ sprintf("%04x", ippct.header_checksum) }"
puts " src addr : #{ ippct.src_ip }"
puts " dst addr : #{ ippct.dst_ip }"
puts " options : #{ ippct.options }"
puts " valid? : #{ ippct.valid? }"
if ippct.protocol == 17 # udp
dgm = ippct.datagram
puts " src port : #{ dgm.src_port }"
puts " dst port : #{ dgm.dst_port }"
puts " total length : #{ dgm.total_length } (octets)"
puts " checksum : #{ sprintf("%04x", dgm.checksum) }"
puts " payload : #{ dgm.payload }"
puts " valid? : #{ dgm.valid? }"
if dgm.dst_port == 7
# udp echo request received, send reply.
send_udp_echo_reply dpid, message, dgm.payload
end
elsif ippct.protocol == 6 # tcp
dgm = ippct.datagram
puts " src port : #{ dgm.src_port }"
puts " dst port : #{ dgm.dst_port }"
puts " seq number : #{ sprintf("%08x", dgm.seq_number) }"
puts " ack number : #{ sprintf("%08x", dgm.ack_number) }"
puts " data offset : #{ dgm.data_offset }"
puts " ctrl flag : #{ sprintf("%06b", dgm.control_flag) }"
puts " window size : #{ sprintf("%04x", dgm.window) }"
puts " checksum : #{ sprintf("%04x", dgm.checksum) }"
puts " urg pointer : #{ sprintf("%04x", dgm.urg_pointer) }"
puts " options : #{ (dgm.optional_tlv.map{ |e| e.tlv_type }).join(", ") }"
puts " payload : #{ dgm.payload }"
puts " valid? : #{ dgm.valid? }"
if dgm.dst_port == 7
s_ip = ippct.src_ip.to_s
s_port = dgm.src_port
unless @tcp_sessions[ s_ip ]
@tcp_sessions[ s_ip ] = {}
end
unless @tcp_sessions[ s_ip ][ s_port ]
srv = Pio::TcpServer.new
srv.send_handler do | reply_dgm |
# reply datagram
send_tcp_reply dpid, message, reply_dgm
end
srv.request_handler do | rcv_dgm, seq, ack |
# generate reply datagram
make_tcp_echo_datagram rcv_dgm, seq, ack
end
@tcp_sessions[ s_ip ][ s_port ] = srv
end
srv = @tcp_sessions[ s_ip ][ s_port ]
srv.receive( dgm )
end
end
end
end
def should_forward?( message )
not @interfaces.find_by_ipaddr( message.ipv4_daddr )
end
def handle_icmpv4_echo_request( dpid, message )
interface = @interfaces.find_by_port( message.in_port )
saddr = message.ipv4_saddr.value
arp_entry = @arp_table.lookup( saddr )
if arp_entry
icmpv4_reply = create_icmpv4_reply( arp_entry, interface, message )
packet_out dpid, icmpv4_reply, SendOutPort.new( interface.port )
else
handle_unresolved_packet dpid, message, interface, saddr
end
end
def forward( dpid, message )
next_hop = resolve_next_hop( message.ipv4_daddr )
interface = @interfaces.find_by_prefix( next_hop )
if not interface or interface.port == message.in_port
return
end
arp_entry = @arp_table.lookup( next_hop )
if arp_entry
macsa = interface.hwaddr
macda = arp_entry.hwaddr
action = create_action_from( macsa, macda, interface.port )
flow_mod dpid, message, action
packet_out dpid, message.data, action
else
handle_unresolved_packet dpid, message, interface, next_hop
end
end
def resolve_next_hop( daddr )
interface = @interfaces.find_by_prefix( daddr.value )
if interface
daddr.value
else
@routing_table.lookup( daddr.value )
end
end
def flow_mod( dpid, message, action )
send_flow_mod_add(
dpid,
:match => ExactMatch.from( message ),
:actions => action
)
end
def packet_out( dpid, packet, action )
send_packet_out(
dpid,
:data => packet,
:actions => action
)
end
def handle_unresolved_packet( dpid, message, interface, ipaddr )
arp_request = Pio::Arp::Request.new(
:source_mac => interface.hwaddr.to_s,
:target_protocol_address => ipaddr.to_s,
:sender_protocol_address => interface.ipaddr.to_s
)
packet_out dpid, arp_request.to_binary, SendOutPort.new( interface.port )
end
def create_action_from( macsa, macda, port )
[
SetEthSrcAddr.new( macsa ),
SetEthDstAddr.new( macda ),
SendOutPort.new( port )
]
end
end
### Local variables:
### mode: Ruby
### coding: utf-8
### indent-tabs-mode: nil
### End:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment