matchd 0.1.0
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 +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
|