blondy-dhcpd 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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