blondy-dhcpd 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae29d05c58be4df1ba9ed1bbb261272d0704fc3e
4
- data.tar.gz: fc31f7a889b49b1c20d0af9d8da1d6ffbe794852
3
+ metadata.gz: cff451085750256df70f852e290cc0486a4662e2
4
+ data.tar.gz: b04ecbb33b1926fdd552572b1f5b4d4e89114c5e
5
5
  SHA512:
6
- metadata.gz: 2574b0e7689854ac759562a3c66a1f67e2e4126f554421c9401c6665e944f36fc46e98d017eef47b84a03357691903d30252459560267ae695ec7b6e3056fcc4
7
- data.tar.gz: 362900db015b131b562e166308fc125207e1c15c43a626c2d0f2a7300128863ac0c1f8fc20590344761dfe6c6a6b63f955ac5080237221fd4bd84e3e884ad401
6
+ metadata.gz: 325c913489b1c2dc5f2030d3f92028b9d0b3336064fba0c0b59e7cacdb814a591fe42bc56dd1f6587ff7b698bfbcb83e3911d029c8d6721c90932668ca1374e2
7
+ data.tar.gz: d1b8714e9e7054d4ddb07b20620178701da0190fb49732e83c0c9dd60a598f6428e199d931c87eb3db1bd603d20907d5ddc1093bc85daf11d0ea335e41c022e0
data/Gemfile CHANGED
@@ -2,11 +2,14 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'net-dhcp', :git => 'https://github.com/mjtko/net-dhcp-ruby'
5
+ gem 'net-dhcp', :git => 'https://github.com/presto53/net-dhcp-ruby', :branch => 'fixed'
6
6
  gem 'log4r'
7
7
  gem 'eventmachine'
8
+ gem 'em-http-request'
8
9
 
9
10
  group :test do
10
11
  gem 'rspec'
12
+ gem 'webmock'
13
+ gem 'simplecov'
11
14
  end
12
15
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Gem Version](https://badge.fury.io/rb/blondy-dhcpd.png)](http://badge.fury.io/rb/net-dhcp) [![Build Status](https://travis-ci.org/presto53/blondy-dhcpd.png)](https://travis-ci.org/presto53/blondy-dhcpd)
1
+ [![Gem Version](https://badge.fury.io/rb/blondy-dhcpd.png)](http://badge.fury.io/rb/net-dhcp) [![Build Status](https://travis-ci.org/presto53/blondy-dhcpd.png)](https://travis-ci.org/presto53/blondy-dhcpd) [![Code Climate](https://codeclimate.com/repos/52eb8ff6e30ba06ec2002a03/badges/132cfa29229385341bee/gpa.png)](https://codeclimate.com/repos/52eb8ff6e30ba06ec2002a03/feed)
2
2
 
3
3
  blondy-dhcpd
4
4
  ============
@@ -6,9 +6,21 @@ DHCPd with remote pools
6
6
 
7
7
  Installation
8
8
  ---------------
9
+ gem install blondy-dhcpd
9
10
 
10
11
  Configuration
11
12
  ---------------
13
+ Default config path is /etc/blondy
14
+
15
+ You can change it by set BLONDY_CONFIGPATH environment variable.
16
+
17
+ **Example config /etc/blondy/dhcpd.yml**:
18
+
19
+ log_path: '/var/log/blondy'
20
+ pid_path: '/var/run/blondy'
21
+ server_ip: '192.168.1.1'
22
+ client_key: 'AAAbbbCcC'
23
+ master: 'https://192.168.1.10'
12
24
 
13
25
  Usage
14
26
  ---------------
data/Rakefile CHANGED
@@ -1 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/TODO.md ADDED
@@ -0,0 +1,3 @@
1
+ TODO
2
+ ---------------
3
+ - TLS for pool hook
data/bin/blondy-dhcpd ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+ require 'eventmachine'
3
+ require 'blondy/dhcpd/logger'
4
+ require 'blondy/dhcpd/config'
5
+ require 'blondy/dhcpd/server'
6
+
7
+ module Blondy
8
+ module DHCPD
9
+ # Check if daemon already running
10
+ if /^\// =~ CONFIG['pid_path']
11
+ @pidf = "#{CONFIG['pid_path'].gsub(/\/*$/,'')}/blondy-dhcpd.pid"
12
+ running_pid = File.open(@pidf, 'r').read.chomp rescue nil
13
+ running_pgid = Process.getpgid(running_pid.to_i) rescue nil if running_pid
14
+ if running_pgid
15
+ Logger.error 'Daemon already running.'
16
+ exit 1
17
+ end
18
+ else
19
+ Logger.error 'PID path is wrong or not set.'
20
+ exit 1
21
+ end
22
+
23
+ # Daemonize
24
+ Process.daemon unless @options[:debug]
25
+
26
+ # Write PID
27
+ File.write(@pidf, "#{Process.pid}\n")
28
+ Logger.info "Starting dhcpd with pid #{Process.pid}"
29
+
30
+ # Signal handlers
31
+ @signals = Array.new
32
+ class << self
33
+ def shutdown(exit_code)
34
+ Logger.info "Shutdown server..."
35
+ EM.stop if EM.reactor_running?
36
+ File.delete(@pidf) if File.exists?(@pidf)
37
+ exit exit_code
38
+ end
39
+ def term_handler
40
+ Logger.info "Server received TERM signal."
41
+ shutdown(0)
42
+ end
43
+ def int_handler
44
+ Logger.info "Server received INT signal."
45
+ shutdown(0)
46
+ end
47
+ end
48
+
49
+ # Start server
50
+ if Process.uid != 0
51
+ Logger.error 'Failed to start server. Server should be started by root.'
52
+ shutdown(1)
53
+ exit 1
54
+ else
55
+ EM.run do
56
+ Signal.trap('TERM') { @signals << :term }
57
+ Signal.trap('INT') { @signals << :int }
58
+ # Check for signals periodically
59
+ EM.add_periodic_timer(1) do
60
+ term_handler if @signals.include?(:term)
61
+ int_handler if @signals.include?(:int)
62
+ end
63
+ EM.open_datagram_socket('0.0.0.0', 67, Server)
64
+ end
65
+ end
66
+ end
67
+ end
data/blondy-dhcpd.gemspec CHANGED
@@ -21,8 +21,11 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
22
  spec.add_development_dependency "rake"
23
23
  spec.add_development_dependency "rspec", "~> 2.0"
24
+ spec.add_development_dependency "webmock"
25
+ spec.add_development_dependency "simplecov"
24
26
 
25
27
  spec.add_runtime_dependency 'eventmachine', '>= 0.12.0'
28
+ spec.add_runtime_dependency 'em-http-request'
26
29
  spec.add_runtime_dependency 'net-dhcp', '>= 1.1.1'
27
30
  spec.add_runtime_dependency 'log4r'
28
31
  end
@@ -0,0 +1,33 @@
1
+
2
+ module Blondy
3
+ module DHCPD
4
+ class Cache
5
+ @cache = Hash.new
6
+ class << self
7
+ def add(hwaddr,type, data)
8
+ @cache[type] = Hash.new unless @cache[type]
9
+ @cache[type][hwaddr] = Hash.new unless @cache[type][hwaddr]
10
+ @cache[type][hwaddr][:data] = data
11
+ @cache[type][hwaddr][:time] = Time.now
12
+ end
13
+ def query(hwaddr, type)
14
+ begin
15
+ @cache[type][hwaddr][:data] ? @cache[type][hwaddr] : false
16
+ rescue
17
+ false
18
+ end
19
+ end
20
+ def flush
21
+ @cache.clear
22
+ end
23
+ def purge(sec)
24
+ @cache.each do |type, data|
25
+ data.each_key do |hwaddr|
26
+ @cache[type].delete hwaddr if (Time.now - @cache[type][hwaddr][:time]) >= sec
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,56 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+ require 'log4r'
4
+
5
+ module Blondy
6
+ module DHCPD
7
+ # Set default config
8
+ default_config = '/etc/blondy/dhcpd.yml'
9
+ config_file = default_config
10
+
11
+ # Read command line options
12
+ @options = Hash.new
13
+ OptionParser.new do |opts|
14
+ opts.banner = 'Usage: dhcpd.rb [options]'
15
+ opts.on('-d', '--debug', 'Run foreground for debug') { @options[:debug] = true }
16
+ opts.on_tail('-h', '--help', 'Show this message') do
17
+ puts opts
18
+ exit 0
19
+ end
20
+ end.parse!
21
+
22
+ # Load config from file
23
+ begin
24
+ config_file = "#{ENV['BLONDY_CONFIGPATH']}/dhcpd.yml" if ENV['BLONDY_CONFIGPATH']
25
+ CONFIG = YAML::load(File.open(config_file))
26
+ rescue
27
+ STDERR.puts "No config file. \nPlease check that #{default_config} exist or BLONDY_CONFIGPATH is set."
28
+ exit 1
29
+ end
30
+
31
+ # Check for client key
32
+ unless CONFIG['client_key']
33
+ Logger.error 'You should set client_key.'
34
+ exit 1
35
+ end
36
+ # Check for master address
37
+ unless /^http(s)?:\/\/.*/ =~ CONFIG['master']
38
+ Logger.error 'You should set master server.'
39
+ exit 1
40
+ end
41
+
42
+ # Set logging to file
43
+ if CONFIG['log_path']
44
+ begin
45
+ if /^\// =~ CONFIG['log_path']
46
+ log_path = CONFIG['log_path'].gsub(/\/*$/,'')
47
+ format = Log4r::PatternFormatter.new(:pattern => "[%l] [%d] %m")
48
+ Logger.outputters << Log4r::FileOutputter.new('blondy-dhcpd.log', filename: "#{log_path}/blondy-dhcpd.log" , formatter: format)
49
+ end
50
+ Logger.level = ((1..7).include?(CONFIG['log_level']) ? CONFIG['log_level'] : 1)
51
+ rescue
52
+ Logger.error 'Error while open log file.'
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,26 +1,97 @@
1
1
  require 'net-dhcp'
2
2
  require 'ostruct'
3
3
  require 'ipaddr'
4
+ require_relative 'pool'
4
5
 
5
6
  module Blondy
6
7
  module DHCPD
7
8
  class Dispatcher
8
- def self.dispatch(data, ip, port)
9
- @data = DHCP::Message.from_udp_payload(data)
10
- reply = OpenStruct.new
11
- if @data.kind_of?(DHCP::Discover)
12
- if @data.giaddr == 0
13
- reply.ip = '255.255.255.255'
14
- reply.port = 68
9
+ class << self
10
+ def dispatch(data, ip, port)
11
+ @data = DHCP::Message.from_udp_payload(data) rescue raise(IncorrectMessage, 'Incorrect message received.')
12
+ raise(IncorrectMessage, 'Incorrect message received.') unless @data
13
+ set_hwaddr
14
+ reply
15
+ end
16
+
17
+ private
18
+
19
+ def reply
20
+ msg_class = @data.class.to_s.gsub(/^.*::/, '').downcase
21
+ handle(msg_class)
22
+ end
23
+
24
+ def handle(msg_class)
25
+ if %w{discover request inform release}.include?(msg_class)
26
+ @pool = Pool.query(@data.hwaddr, msg_class.to_sym)
27
+ @pool ? call("#{msg_class}_handler".to_sym) : false
15
28
  else
16
- reply.ip = IPAddr.new(@data.giaddr, family = Socket::AF_INET).to_s
17
- reply.port = 67
29
+ raise NoMessageHandler, 'No appropriate handler for message.'
18
30
  end
19
- reply.data = DHCP::Offer.new
20
31
  end
21
- reply.data.xid = @data.xid
22
- reply
32
+
33
+ def call(method)
34
+ @reply = OpenStruct.new
35
+ send(method)
36
+ end
37
+
38
+ def discover_handler
39
+ @reply.data = DHCP::Offer.new
40
+ create_reply
41
+ end
42
+
43
+ def request_handler
44
+ @reply.data = DHCP::ACK.new
45
+ create_reply
46
+ end
47
+
48
+ def release_handler
49
+ @reply.data = nil
50
+ end
51
+
52
+ def inform_handler
53
+ @reply.data = DHCP::ACK.new
54
+ create_reply
55
+ end
56
+
57
+ def create_reply
58
+ set_src
59
+ set_pool_data
60
+ set_other
61
+ @reply
62
+ end
63
+
64
+ def set_hwaddr
65
+ DHCP::Message.class_eval {attr_accessor :hwaddr}
66
+ @data.hwaddr = @data.chaddr.take(@data.hlen).map {|x| x.to_s(16).size<2 ? '0'+x.to_s(16) : x.to_s(16)}.join(':')
67
+ end
68
+
69
+ def set_src
70
+ if @data.giaddr == 0 and @data.ciaddr != 0
71
+ @reply.ip = IPAddr.new(@data.ciaddr, family = Socket::AF_INET).to_s
72
+ @reply.port = 68
73
+ elsif @data.giaddr != 0
74
+ @reply.ip = IPAddr.new(@data.giaddr, family = Socket::AF_INET).to_s
75
+ @reply.port = 67
76
+ else
77
+ @reply.ip = '255.255.255.255'
78
+ @reply.port = 68
79
+ end
80
+ end
81
+
82
+ def set_pool_data
83
+ @reply.data.yiaddr = @pool.data.yiaddr
84
+ @reply.data.fname = @pool.data.fname
85
+ @reply.data.options = @pool.data.options
86
+ end
87
+
88
+ def set_other
89
+ @reply.data.siaddr = IPAddr.new(Blondy::DHCPD::CONFIG['server_ip']).to_i
90
+ @reply.data.xid = @data.xid if @data.xid
91
+ end
23
92
  end
24
93
  end
25
94
  end
26
95
  end
96
+
97
+
@@ -0,0 +1,10 @@
1
+ require 'log4r'
2
+
3
+ module Blondy
4
+ module DHCPD
5
+ # Set logger
6
+ Logger = Log4r::Logger.new 'ruby-dhcpd'
7
+ Logger.outputters << Log4r::Outputter.stdout
8
+ end
9
+ end
10
+
@@ -0,0 +1,78 @@
1
+ require 'em-http'
2
+ require 'json'
3
+ require 'ipaddr'
4
+ require_relative 'reply'
5
+ require_relative 'cache'
6
+
7
+ module Blondy
8
+ module DHCPD
9
+ class Pool
10
+ class << self
11
+ def query(hwaddr, type)
12
+ reply = Cache.query(hwaddr,type)
13
+ if reply
14
+ reply[:data]
15
+ else
16
+ http = EM::HttpRequest.new(Blondy::DHCPD::CONFIG['master']).
17
+ get(head: {'x-blondy-key' => Blondy::DHCPD::CONFIG['client_key']}, query: {'type' => type.to_s, 'hwaddr' => hwaddr})
18
+ http.callback do
19
+ if http.response_header.status != 200
20
+ Logger.error "Remote server reply with #{http.response_header.status} error code."
21
+ else
22
+ data = transform(http.response, type)
23
+ Cache.add(hwaddr,type, data) if data
24
+ end
25
+ data
26
+ end
27
+ http.errback do
28
+ Logger.error 'Remote pool server is unavailable.'
29
+ end
30
+ false
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def transform(json, type)
37
+ begin
38
+ data = JSON.parse(json)
39
+ if type == :discover
40
+ reply_type = $DHCP_MSG_OFFER
41
+ elsif type == :request
42
+ reply_type = $DHCP_MSG_ACK
43
+ else
44
+ raise UnsupportedReqType
45
+ end
46
+ Reply.new(data, reply_type).get
47
+ rescue UnsupportedReqType
48
+ # Unsupported request type
49
+ Logger.error 'Unsupported type received.'
50
+ false
51
+ rescue JSON::ParserError
52
+ # Wrong json
53
+ Logger.error 'Remote server send invalid json.'
54
+ false
55
+ rescue NoMethodError
56
+ Logger.error 'Remote server send invalid text data in json.'
57
+ # Wrong data in json
58
+ false
59
+ rescue IPAddr::AddressFamilyError
60
+ # Wrong data in json (address family must be specified)
61
+ Logger.error 'Remote server send invalid ip or nemask in json.'
62
+ false
63
+ rescue IPAddr::InvalidAddressError
64
+ # Wrong data in json (invalid address)
65
+ Logger.error 'Remote server send invalid ip or nemask in json.'
66
+ false
67
+ rescue
68
+ # Unknown error
69
+ Logger.error 'Unknown error.'
70
+ false
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ class UnsupportedReqType < StandardError
78
+ end
@@ -0,0 +1,37 @@
1
+ require 'net-dhcp'
2
+ require 'ostruct'
3
+ require 'ipaddr'
4
+
5
+ module Blondy
6
+ module DHCPD
7
+ class Reply
8
+ def initialize(data, reply_type)
9
+ @result = OpenStruct.new
10
+ @result.data = OpenStruct.new
11
+ @data = data
12
+ @reply_type = reply_type
13
+ @result.data.fname = data['fname'].unpack('C128').map {|x| x ? x : 0}
14
+ @result.data.yiaddr = IPAddr.new(data['yiaddr']).to_i
15
+ @result.data.options = [
16
+ DHCP::MessageTypeOption.new({payload: [@reply_type]}),
17
+ DHCP::ServerIdentifierOption.new({payload: array_from(Blondy::DHCPD::CONFIG['server_ip'])}),
18
+ DHCP::DomainNameOption.new({payload: data['domain'].unpack('C*')}),
19
+ DHCP::DomainNameServerOption.new({payload: array_from(data['dns'])}),
20
+ DHCP::IPAddressLeaseTimeOption.new({payload: [7200].pack('N').unpack('C*')}),
21
+ DHCP::SubnetMaskOption.new({payload: array_from(data['netmask'])}),
22
+ DHCP::RouterOption.new({payload: array_from(data['gw'])})
23
+ ]
24
+ end
25
+
26
+ def get
27
+ @result
28
+ end
29
+
30
+ private
31
+
32
+ def array_from(ip)
33
+ ip.split('.').map {|octet| octet.to_i} if IPAddr.new(ip)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,18 +1,51 @@
1
1
  require 'net-dhcp'
2
2
  require 'eventmachine'
3
3
  require 'socket'
4
+ require 'log4r'
5
+ require_relative 'dispatcher'
4
6
 
5
7
  module Blondy
6
8
  module DHCPD
7
9
  # Main class for handling connections
8
10
  class Server < EM::Connection
11
+ def initialize
12
+ @buffer = String.new
13
+ super
14
+ end
9
15
  # Fires up Dispatcher and send reply back by callback
10
16
  def receive_data(data)
17
+ @buffer.clear if (@buffer.size + data.size) > 1000
18
+ @buffer += data
19
+
20
+ if @buffer.unpack('C4Nn2N4C16C192NC*').include?($DHCP_MAGIC)
21
+ process_message(@buffer.dup)
22
+ @buffer.clear
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def process_message(buffer)
11
29
  ip, port = Socket.unpack_sockaddr_in(get_peername)
12
- action = proc { Dispatcher.dispatch(data, ip, port) }
13
- callback = proc { |reply| send_datagram(reply.data.pack, reply.ip, reply.port) if reply }
30
+ action = proc do
31
+ begin
32
+ Dispatcher.dispatch(buffer, ip, port)
33
+ rescue NoMessageHandler
34
+ Logger.warn 'No handler for message found. Ignore.'
35
+ false
36
+ rescue IncorrectMessage
37
+ Logger.warn 'Incorrect message received. Ignore.'
38
+ false
39
+ end
40
+ end
41
+ callback = proc { |reply| send_datagram(reply.data.pack, reply.ip, reply.port) if reply && reply.data }
14
42
  EM.defer(action,callback)
15
43
  end
16
44
  end
17
45
  end
18
46
  end
47
+
48
+ class NoMessageHandler < StandardError
49
+ end
50
+ class IncorrectMessage < StandardError
51
+ end
@@ -1,5 +1,5 @@
1
1
  module Blondy
2
2
  module Dhcpd
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ module Blondy
4
+ module DHCPD
5
+ describe 'Cache' do
6
+ subject(:cache) {Cache}
7
+ let(:data) do
8
+ pool_query_result = OpenStruct.new
9
+ pool_query_result.data = OpenStruct.new
10
+ pool_query_result.data.fname = 'test.txt'.unpack('C128').map {|x| x ? x : 0}
11
+ pool_query_result.data.yiaddr = IPAddr.new('192.168.5.150').to_i
12
+ pool_query_result.data.options = [
13
+ DHCP::MessageTypeOption.new({payload: [$DHCP_MSG_OFFER]}),
14
+ DHCP::ServerIdentifierOption.new({payload: Blondy::DHCPD::CONFIG['server_ip'].split('.').map {|octet| octet.to_i}}),
15
+ DHCP::DomainNameOption.new({payload: 'example.com'.unpack('C*')}),
16
+ DHCP::DomainNameServerOption.new({payload: [8,8,8,8]}),
17
+ DHCP::IPAddressLeaseTimeOption.new({payload: [7200].pack('N').unpack('C*')}),
18
+ DHCP::SubnetMaskOption.new({payload: [255, 255, 255, 255]}),
19
+ DHCP::RouterOption.new({payload: [192, 168, 1, 3]})
20
+ ]
21
+ pool_query_result
22
+ end
23
+
24
+ before(:each) do
25
+ cache.flush
26
+ end
27
+
28
+ it 'add host to cache' do
29
+ cache.query('11:11:11:11:11:11', :discover).should == false
30
+ cache.add('11:11:11:11:11:11', :discover, data)
31
+ cache.query('11:11:11:11:11:11', :discover)[:data].should == data
32
+ end
33
+ it 'flush cache' do
34
+ cache.add('11:11:11:11:11:11', :discover, data)
35
+ cache.add('12:11:11:11:11:11', :discover, data)
36
+ cache.flush
37
+ cache.query('11:11:11:11:11:11', :discover).should == false
38
+ cache.query('12:11:11:11:11:11', :discover).should == false
39
+ end
40
+ it 'delete entries older than N' do
41
+ @time = Time.now
42
+ Time.stub(:now) {@time}
43
+ cache.add('11:11:11:11:11:11', :discover, data)
44
+ Time.stub(:now) {@time+5}
45
+ cache.purge(3)
46
+ cache.query('11:11:11:11:11:11', :discover).should == false
47
+ end
48
+ end
49
+ end
50
+ end