smart_proxy_dns_dnsmasq 0.4

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.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Dnsmasq Smart Proxy plugin
2
+
3
+
4
+ This plugin adds a new DNS provider for managing records in dnsmasq.
5
+
6
+ ## Installation
7
+
8
+ See [How_to_Install_a_Smart-Proxy_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Smart-Proxy_Plugin)
9
+ for how to install Smart Proxy plugins
10
+
11
+ This plugin is compatible with Smart Proxy 1.15 or higher.
12
+
13
+ ## Configuration
14
+
15
+ To enable this DNS provider, edit `/etc/foreman-proxy/settings.d/dns.yml` and set:
16
+
17
+ :use_provider: dns_dnsmasq
18
+
19
+ Configuration options for this plugin are in `/etc/foreman-proxy/settings.d/dns_dnsmasq.yml` and include:
20
+
21
+ * `backend` (*optional*): The backend to use, currently implemented ones are; `openwrt`, and `default`
22
+ * `config_path`: The path to the configuration file.
23
+ * `reload_cmd`: The command to use for reloading the dnsmasq configuration.
24
+ * `dns_ttl`: The TTL values for the DNS data. (*currently unused*)
25
+
26
+ For best results, `config_path` should point to a file in a dnsmasq `conf-dir` which only the smart-proxy accesses.
27
+
28
+ **NB**: The `openwrt` backend uses the UCI configuration files, which for the moment don't support IPv6 entries.
29
+
30
+ ## Contributing
31
+
32
+ Fork and send a Pull Request. Thanks!
33
+
34
+ ## Copyright
35
+
36
+ Copyright (c) 2017 Alexander Olofsson
37
+
38
+ This program is free software: you can redistribute it and/or modify
39
+ it under the terms of the GNU General Public License as published by
40
+ the Free Software Foundation, either version 3 of the License, or
41
+ (at your option) any later version.
42
+
43
+ This program is distributed in the hope that it will be useful,
44
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
45
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
46
+ GNU General Public License for more details.
47
+
48
+ You should have received a copy of the GNU General Public License
49
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
50
+
@@ -0,0 +1 @@
1
+ gem 'smart_proxy_dns_dnsmasq'
@@ -0,0 +1,9 @@
1
+ ---
2
+ #
3
+ # Configuration file for 'dns_dnsmasq' dns provider
4
+ #
5
+
6
+ #
7
+ #:backend: default
8
+ :config_path: /etc/dnsmasq.d/foreman.conf
9
+ :reload_cmd: systemctl reload dnsmasq
@@ -0,0 +1,3 @@
1
+ require 'smart_proxy_dns_dnsmasq/dns_dnsmasq_plugin'
2
+
3
+ module Proxy::Dns::Dnsmasq; end
@@ -0,0 +1,168 @@
1
+ module Proxy::Dns::Dnsmasq
2
+ class Default < ::Proxy::Dns::Dnsmasq::Record
3
+ attr_reader :config_file, :reload_cmd, :dirty
4
+
5
+ def initialize(config, reload_cmd, dns_ttl)
6
+ @config_file = config
7
+ @reload_cmd = reload_cmd
8
+ @dirty = false
9
+
10
+ super(dns_ttl)
11
+ end
12
+
13
+ def update!
14
+ return unless @dirty
15
+ @dirty = false
16
+
17
+ File.write(@config_file, configuration.join("\n") + "\n")
18
+ system(@reload_cmd)
19
+ end
20
+
21
+ def add_entry(type, fqdn, ip)
22
+ case type
23
+ when 'A', 'AAAA'
24
+ e = AddressEntry.new
25
+ e.ip = ip
26
+ e.fqdn = [fqdn]
27
+ when 'PTR'
28
+ e = PTREntry.new
29
+ e.ip = IPAddr.new(ip).reverse
30
+ e.fqdn = fqdn
31
+ end
32
+
33
+ configuration << e
34
+ @dirty = true
35
+ end
36
+
37
+ def remove_entry(type, fqdn = nil, ip = nil)
38
+ return true unless case type
39
+ when 'A', 'AAAA'
40
+ e = configuration.find { |entry| entry.is_a?(AddressEntry) && entry.fqdn.include?(fqdn) }
41
+ when 'PTR'
42
+ e = configuration.find { |entry| entry.is_a?(PTREntry) && entry.fqdn == fqdn }
43
+ end
44
+
45
+ configuration.delete e
46
+ @dirty = true
47
+ end
48
+
49
+ def add_cname(name, canonical)
50
+ # dnsmasq will silently ignore broken CNAME records, even though they stay in config
51
+ # So avoid flooding the configuration if broken CNAME entries are added
52
+ return true if configuration.find { |entry| entry.is_a?(CNAMEEntry) && entry.name == name }
53
+
54
+ c = CNAMEEntry.new
55
+ c.name = name
56
+ c.target = canonical
57
+ configuration << c
58
+ @dirty = true
59
+ end
60
+
61
+ def remove_cname(name)
62
+ c = configuration.find { |entry| entry.is_a?(CNAMEEntry) && entry.name == name }
63
+ return true unless c
64
+
65
+ configuration.delete c
66
+ @dirty = true
67
+ end
68
+
69
+ private
70
+
71
+ def load!
72
+ @configuration = []
73
+ File.open(@config_file).each_line do |line|
74
+ line = line.strip
75
+ next if line.empty? || line.start_with?('#') || !line.include?('=')
76
+
77
+ option, value = line.split('=')
78
+
79
+ case option
80
+ when 'address'
81
+ data = value.split('/')
82
+ data.shift
83
+
84
+ entry = AddressEntry.new
85
+ entry.ip = data.pop
86
+ entry.fqdn = data
87
+ when 'cname'
88
+ data = value.split(',')
89
+
90
+ entry = CNAMEEntry.new
91
+ entry.name = data.shift
92
+ entry.target = data.shift
93
+ entry.ttl = data.shift
94
+ when 'ptr-record'
95
+ data = value.split(',')
96
+
97
+ entry = PTREntry.new
98
+ entry.ip = data[0]
99
+ entry.fqdn = data[1]
100
+ # TODO: Handle these properly
101
+ # when 'host-record'
102
+ # data = value.split(',')
103
+
104
+ # entry = HostEntry.new
105
+ # until data.empty?
106
+ # v = data.pop
107
+ # if !entry.ttl && /\A\d+\z/ === v
108
+ # entry.ttl = v
109
+ # end
110
+
111
+ # begin
112
+ # ip = IPAddr.new(v)
113
+ # entry.ip << v
114
+ # rescue IPAddr::InvalidAddressError
115
+ # entry.fqdn << v
116
+ # end
117
+ # end
118
+ end
119
+
120
+ @configuration << entry if entry
121
+ end
122
+ end
123
+
124
+ def configuration
125
+ load! unless @configuration
126
+ @configuration
127
+ end
128
+
129
+ class AddressEntry
130
+ attr_accessor :fqdn, :ip
131
+ def initialize
132
+ @fqdn = []
133
+ end
134
+
135
+ def to_s
136
+ "address=/#{fqdn.join '/'}/#{ip}"
137
+ end
138
+ end
139
+
140
+ class CNAMEEntry
141
+ attr_accessor :name, :target, :ttl
142
+
143
+ def to_s
144
+ "cname=#{name},#{target}#{ttl && ',' + ttl}"
145
+ end
146
+ end
147
+
148
+ class PTREntry
149
+ attr_accessor :fqdn, :ip
150
+
151
+ def to_s
152
+ "ptr-record=#{ip},#{fqdn}"
153
+ end
154
+ end
155
+
156
+ class HostEntry
157
+ attr_accessor :ttl, :ip, :fqdn
158
+ def initialize
159
+ @fqdn = []
160
+ @ip = []
161
+ end
162
+
163
+ def to_s
164
+ "host-record=#{fqdn.join ','},#{ip.join ','}#{ttl && ',' + ttl}"
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,141 @@
1
+ require 'ipaddr'
2
+
3
+ module Proxy::Dns::Dnsmasq
4
+ class Openwrt < ::Proxy::Dns::Dnsmasq::Record
5
+ attr_reader :config_file, :reload_cmd, :dirty
6
+
7
+ def initialize(config, reload_cmd, dns_ttl)
8
+ @config_file = config
9
+ @reload_cmd = reload_cmd
10
+ @dirty = false
11
+
12
+ super(dns_ttl)
13
+ end
14
+
15
+ def update!
16
+ return unless @dirty
17
+ @dirty = false
18
+
19
+ File.write(@config_file, "\n" + configuration.join("\n") + "\n")
20
+ system(@reload_cmd)
21
+ end
22
+
23
+ def add_entry(type, fqdn, ip)
24
+ raise Proxy::Dns::Error, "OpenWRT UCI can't manage IPv6 entries" if type == 'AAAA' || type == 'PTR' && IPAddr.new(ip).ipv6?
25
+ found = find_type(:domain, :name, fqdn)
26
+ return true if found && found.options[:ip] == ip
27
+
28
+ h = found
29
+ h = DSL::Config.new :domain unless h
30
+ h.options[:name] = fqdn
31
+ h.options[:ip] = ip
32
+
33
+ configuration << h unless found
34
+ @dirty = true
35
+ end
36
+
37
+ def remove_entry(type, fqdn = nil, ip = nil)
38
+ raise Proxy::Dns::Error, "OpenWRT UCI can't manage IPv6 entries" if type == 'AAAA' || type == 'PTR' && IPAddr.new(ip).ipv6?
39
+ return true unless h = find_type(:domain, fqdn && :name || :ip, fqdn || ip)
40
+
41
+ configuration.delete h
42
+ @dirty = true
43
+ end
44
+
45
+ def add_cname(name, canonical)
46
+ found = find_type(:cname, :name, name)
47
+ return true if found && found.options[:target] == canonical
48
+
49
+ c = found
50
+ c = DSL::Config.new :cname unless c
51
+ c.options[:cname] = name
52
+ c.options[:target] = canonical
53
+
54
+ configuration << c unless found
55
+ @dirty = true
56
+ end
57
+
58
+ def remove_cname(name)
59
+ return true unless c = find_type(:cname, :name, name)
60
+
61
+ configuration.delete c
62
+ @dirty = true
63
+ end
64
+
65
+ private
66
+
67
+ def find_type(filter_type, search_type, value)
68
+ configuration.find do |config|
69
+ next unless config.type == filter_type
70
+
71
+ config.options.find do |name, opt|
72
+ next unless name == search_type
73
+
74
+ opt == value
75
+ end
76
+ end
77
+ end
78
+
79
+ def load!
80
+ @configuration = []
81
+ dsl = DSL.new(@configuration)
82
+ dsl.instance_eval open(@config_file).read, @config_file
83
+ end
84
+
85
+ def configuration
86
+ load! unless @configuration
87
+ @configuration
88
+ end
89
+
90
+ class DSL
91
+ class Config
92
+ attr_reader :type, :name, :options
93
+
94
+ def initialize(type, name = nil)
95
+ @type = type.to_sym
96
+ @name = name
97
+ @options = {}
98
+ end
99
+
100
+ def to_s
101
+ "config #{type} #{name}\n" + options.map do |name, value|
102
+ if value.is_a? Array
103
+ value.map do|val|
104
+ " list #{name} '#{val}'"
105
+ end.join "\n"
106
+ else
107
+ " option #{name} '#{value}'"
108
+ end
109
+ end.join("\n") + "\n"
110
+ end
111
+ end
112
+
113
+ def initialize(config)
114
+ @configs = config
115
+ end
116
+
117
+ def method_missing(m, *args)
118
+ [m, args].flatten
119
+ end
120
+
121
+ def config(args)
122
+ type, name = args
123
+ @current_config = Config.new type, name
124
+ @configs << @current_config
125
+ end
126
+
127
+ def option(args)
128
+ name, value = args
129
+
130
+ @current_config.options[name] = value
131
+ end
132
+
133
+ def list(args)
134
+ name, value = args
135
+
136
+ @current_config.options[name] = [] unless @current_config.options[name]
137
+ @current_config.options[name] << value
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,37 @@
1
+ module ::Proxy::Dns::Dnsmasq
2
+ class PluginConfiguration
3
+ def load_classes
4
+ require 'dns_common/dns_common'
5
+ require 'smart_proxy_dns_dnsmasq/dns_dnsmasq_main'
6
+ end
7
+
8
+ BACKENDS = [ 'openwrt', 'default' ].freeze
9
+ def load_dependency_injection_wirings(container_instance, settings)
10
+ backend = settings[:backend] || 'default'
11
+
12
+ unless BACKENDS.include? backend
13
+ raise ::Proxy::Error::ConfigurationError, 'In'
14
+ end
15
+
16
+ begin
17
+ require "smart_proxy_dns_dnsmasq/backend/#{backend}"
18
+ rescue LoadError, e
19
+ raise ::Proxy::Error::ConfigurationError, "Failed to load backend #{backend}: #{e}"
20
+ end
21
+
22
+ klass = case backend
23
+ when 'openwrt'
24
+ ::Proxy::Dns::Dnsmasq::Openwrt
25
+ when 'default'
26
+ ::Proxy::Dns::Dnsmasq::Default
27
+ end
28
+
29
+ container_instance.dependency :dns_provider, (lambda do
30
+ klass.new(
31
+ settings[:config_path],
32
+ settings[:reload_cmd],
33
+ settings[:dns_ttl])
34
+ end)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ require 'dns_common/dns_common'
2
+ require 'ipaddr'
3
+
4
+ module Proxy::Dns::Dnsmasq
5
+ class Record < ::Proxy::Dns::Record
6
+ include Proxy::Log
7
+
8
+ def initialize(dns_ttl)
9
+ super('localhost', dns_ttl)
10
+ end
11
+
12
+ def do_create(name, value, type)
13
+ case type
14
+ when 'A', 'AAAA'
15
+ add_entry(type, name, value)
16
+ when 'PTR'
17
+ add_entry(type, value, ptr_to_ip(name))
18
+ when 'CNAME'
19
+ add_cname(name, value)
20
+ else
21
+ raise Proxy::Dns::Error, "Can't create entries of type #{type}"
22
+ end
23
+
24
+ update!
25
+ end
26
+
27
+ def do_remove(name, type)
28
+ case type
29
+ when 'A', 'AAAA'
30
+ remove_entry(type, name)
31
+ when 'PTR'
32
+ remove_entry(type, nil, ptr_to_ip(name))
33
+ when 'CNAME'
34
+ remove_cname(name)
35
+ else
36
+ raise Proxy::Dns::Error, "Can't remove entries of type #{type}"
37
+ end
38
+
39
+ update!
40
+ end
41
+ end
42
+ end