matchd 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop-https---relaxed-ruby-style-rubocop-yml +166 -0
- data/.rubocop.yml +20 -0
- data/.travis.yml +18 -0
- data/DEVELOPMENT.md +5 -0
- data/Gemfile +8 -0
- data/License.txt +373 -0
- data/README.md +473 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/matchd +4 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/examples/registry.yml +30 -0
- data/exe/matchd +6 -0
- data/lib/matchd.rb +31 -0
- data/lib/matchd/cli.rb +10 -0
- data/lib/matchd/cli/config.rb +36 -0
- data/lib/matchd/cli/config_file_option.rb +23 -0
- data/lib/matchd/cli/main.rb +45 -0
- data/lib/matchd/config.rb +39 -0
- data/lib/matchd/control.rb +56 -0
- data/lib/matchd/glue.rb +6 -0
- data/lib/matchd/glue/async_endpoint.rb +93 -0
- data/lib/matchd/helpers.rb +14 -0
- data/lib/matchd/registry.rb +66 -0
- data/lib/matchd/response.rb +72 -0
- data/lib/matchd/response/a.rb +12 -0
- data/lib/matchd/response/aaaa.rb +5 -0
- data/lib/matchd/response/cname.rb +12 -0
- data/lib/matchd/response/mx.rb +16 -0
- data/lib/matchd/response/ns.rb +12 -0
- data/lib/matchd/response/ptr.rb +12 -0
- data/lib/matchd/response/soa.rb +28 -0
- data/lib/matchd/response/srv.rb +20 -0
- data/lib/matchd/response/txt.rb +12 -0
- data/lib/matchd/rule.rb +116 -0
- data/lib/matchd/rule/append.rb +23 -0
- data/lib/matchd/rule/fail.rb +18 -0
- data/lib/matchd/rule/invalid.rb +24 -0
- data/lib/matchd/rule/passthrough.rb +50 -0
- data/lib/matchd/rule/respond.rb +16 -0
- data/lib/matchd/server.rb +44 -0
- data/lib/matchd/version.rb +3 -0
- data/matchd.gemspec +31 -0
- metadata +191 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "matchd"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/matchd
ADDED
data/bin/rake
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rspec' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/setup
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
---
|
2
|
+
version: 1
|
3
|
+
rules:
|
4
|
+
# # exact match:
|
5
|
+
# - match: mydomain.test
|
6
|
+
# resource_class: A
|
7
|
+
# respond: "10.0.0.80"
|
8
|
+
#
|
9
|
+
# # using ruby regular expressions to match any sub-domains
|
10
|
+
# - match: /^(\w+\.)*mydomain\.test$/
|
11
|
+
# resource_class: A
|
12
|
+
# respond: "10.0.0.80"
|
13
|
+
#
|
14
|
+
# # IPv6 records
|
15
|
+
# - match: /^(\w+\.)*mydomain\.test$/
|
16
|
+
# resource_class: AAAA
|
17
|
+
# respond: "fe80::A:0:0:0050"
|
18
|
+
#
|
19
|
+
# # MX records
|
20
|
+
# - match: mydomain.test
|
21
|
+
# resource_class: MX
|
22
|
+
# respond:
|
23
|
+
# preference: 10
|
24
|
+
# host: 'mail.mydomain.test.'
|
25
|
+
#
|
26
|
+
# - match: mydomain.test
|
27
|
+
# resource_class: NS
|
28
|
+
# respond:
|
29
|
+
# - host: 'ns1.mydomain.test.'
|
30
|
+
# - host: 'ns2.mydomain.test.'
|
data/exe/matchd
ADDED
data/lib/matchd.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "matchd/version"
|
2
|
+
|
3
|
+
module Matchd
|
4
|
+
autoload :Config, "matchd/config"
|
5
|
+
autoload :Control, "matchd/control"
|
6
|
+
autoload :Glue, "matchd/glue"
|
7
|
+
autoload :Helpers, "matchd/helpers"
|
8
|
+
autoload :Registry, "matchd/registry"
|
9
|
+
autoload :Response, "matchd/response"
|
10
|
+
autoload :Rule, "matchd/rule"
|
11
|
+
autoload :Server, "matchd/server"
|
12
|
+
|
13
|
+
def self.configure(&block)
|
14
|
+
Config.configure(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.configure_from_file!(config_file = Config::DEFAULT_CONFIG_FILE)
|
18
|
+
Config.configure do |config|
|
19
|
+
YAML.load_file(config_file).each do |k, v|
|
20
|
+
config.public_send("#{k}=", v)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.root
|
26
|
+
File.expand_path('..', __dir__)
|
27
|
+
end
|
28
|
+
|
29
|
+
extend Response::Factory
|
30
|
+
extend Rule::Factory
|
31
|
+
end
|
data/lib/matchd/cli.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Matchd::CLI
|
2
|
+
class Config < Thor
|
3
|
+
include Thor::Actions
|
4
|
+
|
5
|
+
add_runtime_options!
|
6
|
+
class_option :verbose, type: :boolean, aliases: "-v", group: :runtime,
|
7
|
+
desc: "Print out additional logging information"
|
8
|
+
|
9
|
+
desc "setup [options]", "Creates the basic configuration files"
|
10
|
+
option :base,
|
11
|
+
aliases: "-b",
|
12
|
+
type: :string,
|
13
|
+
default: Matchd::Config::DEFAULT_DOT_DIR,
|
14
|
+
desc: "the base directory for all config files"
|
15
|
+
option :config_file,
|
16
|
+
aliases: "-C",
|
17
|
+
type: :string,
|
18
|
+
default: Matchd::Config::DEFAULT_CONFIG_FILE,
|
19
|
+
desc: "Name of the config file to create. Relative to 'base' or absolute path"
|
20
|
+
def setup
|
21
|
+
opts = options.dup
|
22
|
+
|
23
|
+
dot_dir = File.expand_path(opts.delete(:base))
|
24
|
+
config_file = File.expand_path(opts.delete(:config_file), dot_dir)
|
25
|
+
|
26
|
+
Matchd.configure { |c| c.dot_dir = dot_dir }
|
27
|
+
|
28
|
+
empty_directory(dot_dir, opts)
|
29
|
+
|
30
|
+
create_file(config_file, YAML.dump(Matchd::Config.config.to_h), opts)
|
31
|
+
|
32
|
+
sample_registry = File.expand_path(File.join("examples", "registry.yml"), Matchd.root)
|
33
|
+
create_file(Matchd::Config.registry_file, File.binread(sample_registry), opts)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Matchd::CLI
|
2
|
+
# A little patch to allow a "class_option" for letting Matchd be configured
|
3
|
+
# using the given or default config file
|
4
|
+
module ConfigFileOption
|
5
|
+
def self.included(receiver)
|
6
|
+
receiver.class_exec do
|
7
|
+
class_option :config,
|
8
|
+
type: :string,
|
9
|
+
aliases: "-c",
|
10
|
+
group: :runtime,
|
11
|
+
default: Matchd::Config::DEFAULT_CONFIG_FILE,
|
12
|
+
desc: "The config file to read"
|
13
|
+
|
14
|
+
no_commands do
|
15
|
+
def initialize(args = [], local_options = {}, config = {})
|
16
|
+
super
|
17
|
+
Matchd.configure_from_file!(options[:config]) if File.file?(options[:config])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "matchd"
|
2
|
+
|
3
|
+
module Matchd::CLI
|
4
|
+
class Main < Thor
|
5
|
+
package_name "Matchd"
|
6
|
+
|
7
|
+
include ConfigFileOption
|
8
|
+
|
9
|
+
desc "start [options]", "Start the matchd dns service"
|
10
|
+
option :deamonize,
|
11
|
+
default: true,
|
12
|
+
type: :boolean,
|
13
|
+
desc: "Start as background daemon."
|
14
|
+
def start
|
15
|
+
Matchd::Control.new.start(ontop: !options[:deamonize])
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "stop", "Stop the running matchd dns daemon"
|
19
|
+
def stop
|
20
|
+
Matchd::Control.new.stop
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "status", "Print process status information"
|
24
|
+
def status
|
25
|
+
Matchd::Control.new.status
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "restart", "Restart the running matchd daemon"
|
29
|
+
long_desc "Stop and Start with new options. " \
|
30
|
+
"This is the same as running stop and start successively.\n\n" \
|
31
|
+
"If your configuration changes the 'dot_dir' you'll need to stop using the old config and start with the new one."
|
32
|
+
option :deamonize,
|
33
|
+
default: true,
|
34
|
+
type: :boolean,
|
35
|
+
desc: "Restart as background daemon.",
|
36
|
+
long_desc: ""
|
37
|
+
def restart
|
38
|
+
invoke :stop
|
39
|
+
invoke :start
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "config SUBCOMMAND ...ARGS", "manage configuration files"
|
43
|
+
subcommand "config", Config
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "dry-configurable"
|
3
|
+
|
4
|
+
module Matchd
|
5
|
+
module Config
|
6
|
+
DEFAULT_PORT = 15353
|
7
|
+
DEFAULT_DOT_DIR = File.join(ENV["HOME"], ".matchd")
|
8
|
+
DEFAULT_CONFIG_FILE = File.join(DEFAULT_DOT_DIR, "config.yml")
|
9
|
+
|
10
|
+
extend Dry::Configurable
|
11
|
+
|
12
|
+
# Base directory for config and daemon files
|
13
|
+
setting(:dot_dir, DEFAULT_DOT_DIR, reader: true) { |f| File.expand_path(f) }
|
14
|
+
|
15
|
+
# where the server will listen on
|
16
|
+
setting(
|
17
|
+
:listen,
|
18
|
+
[
|
19
|
+
{ "protocol" => "udp", "ip" => "127.0.0.1", "port" => DEFAULT_PORT },
|
20
|
+
{ "protocol" => "udp", "ip" => "::1", "port" => DEFAULT_PORT }
|
21
|
+
],
|
22
|
+
reader: true
|
23
|
+
)
|
24
|
+
|
25
|
+
# The upstream resolver for passthrough requests.
|
26
|
+
# "system" will try to read your systems DNS setup.
|
27
|
+
# Give a specific config like this:
|
28
|
+
#
|
29
|
+
# [{"protocol" => "udp", "ip" => "1.1.1.1", "port" => 53},
|
30
|
+
# {"protocol" => "tcp", "ip" => "1.1.1.1", "port" => 53}]
|
31
|
+
#
|
32
|
+
setting(:resolver, "system", reader: true)
|
33
|
+
|
34
|
+
setting(:registry_file, "registry.yml")
|
35
|
+
def self.registry_file
|
36
|
+
File.expand_path(config.registry_file, dot_dir)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "daemons"
|
2
|
+
|
3
|
+
module Matchd
|
4
|
+
# Controls the demonizing of a {Matchd::Server}
|
5
|
+
class Control
|
6
|
+
def initialize(options = {})
|
7
|
+
@name = options.delete(:name, "matchd")
|
8
|
+
end
|
9
|
+
|
10
|
+
def start(options = {})
|
11
|
+
run! "start", ontop: options.fetch(:ontop, false)
|
12
|
+
end
|
13
|
+
|
14
|
+
def stop
|
15
|
+
run! "stop"
|
16
|
+
end
|
17
|
+
|
18
|
+
def status
|
19
|
+
run! "status"
|
20
|
+
end
|
21
|
+
|
22
|
+
def restart(options = {})
|
23
|
+
stop
|
24
|
+
start(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @private
|
28
|
+
def run!(argv, options = {})
|
29
|
+
run_options = { ARGV: Array(argv), **options, **daemon_opts }
|
30
|
+
Daemons.run_proc(@name, run_options) do
|
31
|
+
require "matchd/server"
|
32
|
+
Matchd::Server.new(*server_opts).run
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @private
|
37
|
+
def server_opts
|
38
|
+
[
|
39
|
+
Matchd::Registry.load_file(Matchd::Config.registry_file),
|
40
|
+
Matchd::Config.listen,
|
41
|
+
{ resolver: Matchd::Config.resolver }
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
# @private
|
46
|
+
def daemon_opts
|
47
|
+
daemon_dir = Matchd::Config.dot_dir
|
48
|
+
{
|
49
|
+
dir_mode: :normal,
|
50
|
+
dir: daemon_dir,
|
51
|
+
log_output: true,
|
52
|
+
log_dir: daemon_dir
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/matchd/glue.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
module Matchd::Glue
|
2
|
+
# Wrapper for allowing a more flexible way of defining interfaces for Asyc-*
|
3
|
+
module AsyncEndpoint
|
4
|
+
class << self
|
5
|
+
# Examples:
|
6
|
+
#
|
7
|
+
# # classic triplet array
|
8
|
+
# AsyncIntrface.parse([:udp, "0.0.0.0", 53])
|
9
|
+
# AsyncIntrface.parse([[:udp, "0.0.0.0", 53], [:tcp, "0.0.0.0", 53]])
|
10
|
+
#
|
11
|
+
# # Hash notation
|
12
|
+
# AsyncIntrface.parse({"protocol" => :udp, "ip" => "0.0.0.0", "port" => 53})
|
13
|
+
# AsyncIntrface.parse({protocol: :udp, ip: "0.0.0.0", port: 53})
|
14
|
+
# AsyncIntrface.parse([{protocol: :udp, ip: "0.0.0.0", port: 53}, {protocol: :tcp, ip: "0.0.0.0", port: 53}])
|
15
|
+
#
|
16
|
+
# # URI strings
|
17
|
+
# AsyncIntrface.parse("udp://0.0.0.0:53")
|
18
|
+
# AsyncIntrface.parse(["udp://0.0.0.0:53", "tcp://0.0.0.0:53"])
|
19
|
+
def parse(args)
|
20
|
+
val =
|
21
|
+
case args
|
22
|
+
when Array
|
23
|
+
if triplet = parse_array(args)
|
24
|
+
[triplet]
|
25
|
+
else
|
26
|
+
args.flat_map { |e| parse(e) }
|
27
|
+
end
|
28
|
+
when Hash
|
29
|
+
[parse_hash(args)]
|
30
|
+
when String
|
31
|
+
[parse_uri(args)]
|
32
|
+
end
|
33
|
+
|
34
|
+
return nil unless val
|
35
|
+
|
36
|
+
val.compact!
|
37
|
+
|
38
|
+
val.empty? ? nil : val
|
39
|
+
end
|
40
|
+
|
41
|
+
# Supported DNS protocols
|
42
|
+
PROTOCOLS = %w(udp tcp).freeze
|
43
|
+
|
44
|
+
# Default port used when the port is omitted in Hash or String notation.
|
45
|
+
DEFAULT_PORT = 53
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# @private
|
50
|
+
# parses the classic triplet array notation
|
51
|
+
# `[:udp, "0.0.0.0", 53]`
|
52
|
+
# and returns just the thing if all data is present.
|
53
|
+
# To ensure proper triplet detection, all values are required. No port default.
|
54
|
+
def parse_array(args)
|
55
|
+
protocol, ip, port = args
|
56
|
+
|
57
|
+
return nil unless PROTOCOLS.include?(protocol.to_s) && ip && port
|
58
|
+
|
59
|
+
[protocol.to_sym, ip, port]
|
60
|
+
end
|
61
|
+
|
62
|
+
# @private
|
63
|
+
# parses a single Hash with named components in string or symbol keys
|
64
|
+
# `{"protocol" => :udp, "ip" => "0.0.0.0", "port" => 53}` or
|
65
|
+
# `{protocol: :udp, ip: "0.0.0.0", port: 53}`
|
66
|
+
# and returns it's triplet notation if all parts are present.
|
67
|
+
# there's no additional array wrapping.
|
68
|
+
def parse_hash(args)
|
69
|
+
protocol = args["protocol"] || args[:protocol]
|
70
|
+
ip = args["ip"] || args[:ip]
|
71
|
+
port = args["port"] || args[:port] || DEFAULT_PORT
|
72
|
+
|
73
|
+
return nil unless PROTOCOLS.include?(protocol.to_s) && ip
|
74
|
+
|
75
|
+
[protocol.to_sym, ip, port]
|
76
|
+
end
|
77
|
+
|
78
|
+
# @private
|
79
|
+
# parses a URI string
|
80
|
+
# `"udp://0.0.0.0:53"` or `"tcp://0.0.0.0:53"`
|
81
|
+
def parse_uri(args)
|
82
|
+
uri = URI.parse(args)
|
83
|
+
protocol = uri.scheme
|
84
|
+
ip = uri.host
|
85
|
+
port = uri.port || DEFAULT_PORT
|
86
|
+
|
87
|
+
return nil unless PROTOCOLS.include?(protocol.to_s) && ip
|
88
|
+
|
89
|
+
[protocol.to_sym, ip, port]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|