Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
arcage committed Dec 10, 2018
0 parents commit 67b3a9e
Show file tree
Hide file tree
Showing 16 changed files with 593 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
@@ -0,0 +1,9 @@
root = true

[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
9 changes: 9 additions & 0 deletions .gitignore
@@ -0,0 +1,9 @@
/docs/
/lib/
/bin/
/.shards/
*.dwarf

# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
/shard.lock
1 change: 1 addition & 0 deletions .travis.yml
@@ -0,0 +1 @@
language: crystal
21 changes: 21 additions & 0 deletions LICENSE
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2018 ʕ·ᴥ·ʔAKJ

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
50 changes: 50 additions & 0 deletions README.md
@@ -0,0 +1,50 @@
# Network programing samples for the Crystal language

This shard includes following the sample codes.

- Gets hardware addresses(MAC address / phisical address) of the network interfaces.

Implemented for linux(tested of CentOS7) and BSD(tested ofMac OS 10.14).

- `ping` like command

Sends ICMP ECHO request and receive its REPLY.

_NOTE: Included codes are not practical but only samples._

## Installation

1. Add the dependency to your `shard.yml`
```yaml
dependencies:
net_sample:
github: arcage/net_sample.cr
```
2. Run `shards install`

## Usage

```crystal
require "net_sample"
# get HW address from given network interface name
mac_of_eth1 = NetSample::HWAddr.hwaddr_of("eth1")
# ping like command
NetSample::Ping.command("192.0.2.1")
#=>
# PING 192.0.2.1: 56 data bytes
# 64 bytes from 192.0.2.1: icmp_seq=0 ttl=253 time=1.903 ms
# 64 bytes from 192.0.2.1: icmp_seq=1 ttl=253 time=1.466 ms
# 64 bytes from 192.0.2.1: icmp_seq=2 ttl=253 time=1.355 ms
# 64 bytes from 192.0.2.1: icmp_seq=3 ttl=253 time=2.115 ms
# 64 bytes from 192.0.2.1: icmp_seq=4 ttl=253 time=2.074 ms
#
# --- 192.0.2.1 ping statistics ---
# 5 packets transmitted, 5 packets received, 0% packet loss
# round-trip min/avg/max/stddev = 1.355/1.783/2.115/0.346 ms
```

## Contributors

- [ʕ·ᴥ·ʔAKJ](https://github.com/arcage) - creator and maintainer
9 changes: 9 additions & 0 deletions shard.yml
@@ -0,0 +1,9 @@
name: net_sample
version: 0.1.0

authors:
- ʕ·ᴥ·ʔAKJ <arcage@denchu.org>

crystal: 0.27.0

license: MIT
9 changes: 9 additions & 0 deletions spec/net_sample_spec.cr
@@ -0,0 +1,9 @@
require "./spec_helper"

describe NetSample do
# TODO: Write tests

it "works" do
false.should eq(true)
end
end
2 changes: 2 additions & 0 deletions spec/spec_helper.cr
@@ -0,0 +1,2 @@
require "spec"
require "../src/net_sample"
7 changes: 7 additions & 0 deletions src/net_sample.cr
@@ -0,0 +1,7 @@
require "socket"

module NetSample
VERSION = "0.1.0"
end

require "./net_sample/*"
49 changes: 49 additions & 0 deletions src/net_sample/hwaddr.cr
@@ -0,0 +1,49 @@
lib LibC
IFHWADDRLEN = 6
end

class NetSample::HWAddr
@@hwaddrs : Hash(String, self)?

def self.hwaddrs
@@hwaddrs ||= get_hwaddr
end

def self.hwaddr_of(if_name : String) : self
hwaddr_of?(if_name) || raise "#{if_name} not exist."
end

def self.hwaddr_of?(if_name : String) : self | Nil
hwaddrs[if_name]?
end

def self.if_names : Array(String)
hwaddrs.keys
end

def initialize(@bytes : Bytes)
end

def byte_size
@bytes.size
end

def to_bytes
@bytes
end

def to_s(io)
i = 0
loop do
io << ("%02x" % @bytes[i])
i += 1
if i == byte_size
break
end
io << ':'
end
io << ":??" unless byte_size == LibC::IFHWADDRLEN
end
end

require "./hwaddr/*"
61 changes: 61 additions & 0 deletions src/net_sample/hwaddr/bsd.cr
@@ -0,0 +1,61 @@
{% skip_file unless flag?(:bsd) || flag?(:darwin) %}

lib LibC
AF_LINK = 18
SDL_DATA_SIZE = 32

struct SockaddrDl
sdl_len : UChar
sdl_family : UChar
sdl_index : UShort
sdl_type : UChar
sdl_nlen : UChar
sdl_alen : UChar
sdl_slen : UChar
sdl_data : Char[SDL_DATA_SIZE]
{% if flag?(:darwin) %}
sdl_rcf : UShort
sdl_route : UShort[16]
{% end %}
end

struct Ifaddrs
ifa_next : Ifaddrs*
ifa_name : Char*
ifa_flags : UInt
ifa_addr : Sockaddr*
ifa_netmask : Sockaddr*
ifa_dstaddr : Sockaddr*
ifa_data : Void*
end

fun getifaddrs(ifaddr : Ifaddrs*) : Int
end

class NetSample::HWAddr
private def self.get_hwaddr : Hash(String, self)
hwaddrs = {} of String => self
ifa = LibC::Ifaddrs.new
ifap = pointerof(ifa)
LibC.getifaddrs(ifap)
while ifap
ifa = ifap.value
if ifa.ifa_addr
dl = ifa.ifa_addr.as(LibC::SockaddrDl*).value
if dl.sdl_family == LibC::AF_LINK
nlen = dl.sdl_nlen
alen = dl.sdl_alen
if alen == LibC::IFHWADDRLEN
data = dl.sdl_data.to_slice.clone
alen = nlen > LibC::SDL_DATA_SIZE - LibC::IFHWADDRLEN ? LibC::SDL_DATA_SIZE - nlen : LibC::IFHWADDRLEN
if_name = String.new(data[0, nlen])
hwaddr = data[nlen, alen]
hwaddrs[if_name] = self.new(hwaddr)
end
end
end
ifap = ifa.ifa_next
end
hwaddrs
end
end
83 changes: 83 additions & 0 deletions src/net_sample/hwaddr/linux.cr
@@ -0,0 +1,83 @@
{% skip_file unless flag?(:linux) %}

lib LibC
IF_NAMESIZE = 16
SIOCGIFHWADDR = 0x8927
SIOCGIFCONF = 0x8912
MAX_IFS = 32

struct Ifmap
mem_start : ULong
mem_end : ULong
base_addr : UShort
irq : UChar
dma : UChar
port : UChar
end

union IfreqU
ifr_addr : Sockaddr
ifr_dstaddr : Sockaddr
ifr_broadaddr : Sockaddr
ifr_netmask : Sockaddr
ifr_hwaddr : Sockaddr
ifr_flags : Short
ifr_ifindex : Int
ifr_metric : Int
ifr_mtu : Int
ifr_map : Ifmap
ifr_slave : Char[IF_NAMESIZE]
ifr_newname : Char[IF_NAMESIZE]
ifr_data : Char*
end

struct Ifreq
ifr_name : Char[IF_NAMESIZE]
ifr_req_u : IfreqU
end

alias IfconfReq = StaticArray(Ifreq, MAX_IFS)

union IfconfU
ifc_buf : Char*
ifc_req : IfconfReq*
end

struct Ifconf
ifc_len : Int
ifc_u : IfconfU
end

fun ioctl(fd : Int, request : Int, value_result : Void*) : Int
end

class NetSample::HWAddr
private def self.get_hwaddr : Hash(String, self)
if_names = [] of String
hwaddrs = {} of String => self
ifconf = LibC::Ifconf.new
ifconf_req = LibC::IfconfReq.new { LibC::Ifreq.new }
ifconf.ifc_len = sizeof(LibC::IfconfReq)
ifconf.ifc_u.ifc_req = pointerof(ifconf_req)
socket = LibC.socket(LibC::AF_INET, LibC::SOCK_DGRAM, 0)
if LibC.ioctl(socket, LibC::SIOCGIFCONF, pointerof(ifconf).as(Void*)) < 0
raise "Error: #{Errno.new(Errno.value)}\n"
end
ifconf_req.each do |ifreq|
ifr_addr = ifreq.ifr_req_u.ifr_addr
if ifr_addr.sa_family == LibC::AF_INET
if_nlen = ifreq.ifr_name.to_slice.index { |b| b == 0u8 } || LibC::IF_NAMESIZE
if_name = String.new(ifreq.ifr_name.to_slice[0, if_nlen])
hwareq = LibC::Ifreq.new
hwareq.ifr_req_u.ifr_addr.sa_family = LibC::AF_INET
hwareq.ifr_name = ifreq.ifr_name
if LibC.ioctl(socket, LibC::SIOCGIFHWADDR, pointerof(hwareq).as(Void*)) < 0
raise "Error: #{Errno.new(Errno.value)}"
end
hwaddr = hwareq.ifr_req_u.ifr_addr.sa_data.to_slice[0, LibC::IFHWADDRLEN].clone
hwaddrs[if_name] = self.new(hwaddr)
end
end
hwaddrs
end
end
35 changes: 35 additions & 0 deletions src/net_sample/ping.cr
@@ -0,0 +1,35 @@
require "math"
require "./ping/icmp"
require "./ping/client"
require "./ping/result"

# TODO: Write documentation for `Ping`
module NetSample::Ping
VERSION = "0.1.0"

class Error < Exception; end

def self.command(host : String, *, count : UInt16 = 5u16, data = "ping!")
raise Error.new("ping count must be greater than 0.") unless count > 0
data_length = [56, data.size].max
puts "PING #{host}: #{data_length} data bytes"

client = Ping::Client.new(host)
id = Process.pid.to_u16
sequence = 0u16
rtt_list = [] of Float64
while sequence < count
res = client.ping(id, sequence, data)
puts res.message
rtt_list << res.rtt if res.is_a?(EchoReplyReceived) && res.valid?
sleep(1)
sequence += 1
end
received = rtt_list.size
puts "\n--- #{host} ping statistics ---"
puts "#{count} packets transmitted, #{received} packets received, #{"%.1d" % (100.0 - (received.to_f * 100 / count))}% packet loss"
avg = rtt_list.reduce { |a, i| a + i } / received
stddev = Math.sqrt(rtt_list.map { |rtt| rtt - avg }.reduce { |a, i| a + (i ** 2) } / received)
puts "round-trip min/avg/max/stddev = #{"%.3f" % rtt_list.min}/#{"%.3f" % avg}/#{"%.3f" % rtt_list.max}/#{"%.3f" % stddev} ms"
end
end
33 changes: 33 additions & 0 deletions src/net_sample/ping/client.cr
@@ -0,0 +1,33 @@
module NetSample::Ping
class Client
class IllegalReplyReceived < Exception; end

@socket : Socket
@host : Socket::IPAddress

def initialize(host : String)
@host = Socket::IPAddress.new(host, 0)
@socket = Socket.new(Socket::Family::INET,
Socket::Type::RAW,
Socket::Protocol::ICMP)
@socket.read_timeout = 5.second
end

def ping(id : UInt16, sequence : UInt16, message : String)
send_icmp = ICMP::EchoRequest.new(id, sequence, message)
bytes = send_icmp.to_bytes
begin
LibC.sendto(@socket.fd, bytes.to_unsafe.as(Void*), bytes.size, 0, @host, @host.size)
send_time = Time.now
bytes = Bytes.new(1500)
bytes_read, _ = @socket.receive(bytes)
rescue ex
return Result.load(send_icmp, ex)
end
receive_time = Time.now
rtt = (receive_time - send_time).total_milliseconds
packet = ICMP::Packet.new(bytes[0, bytes_read])
return Result.load(send_icmp, packet, rtt)
end
end
end

0 comments on commit 67b3a9e

Please sign in to comment.