├── .ruby-version ├── lib ├── vagrant-dns │ ├── version.rb │ ├── installers.rb │ ├── middlewares │ │ ├── config_up.rb │ │ ├── config_down.rb │ │ └── restart.rb │ ├── registry.rb │ ├── tld_registry.rb │ ├── store.rb │ ├── installers │ │ ├── linux.rb │ │ └── mac.rb │ ├── service.rb │ ├── server.rb │ ├── config.rb │ ├── command.rb │ └── configurator.rb └── vagrant-dns.rb ├── Rakefile ├── testdrive ├── bin │ └── vagrant └── Vagrantfile ├── .gitignore ├── .editorconfig ├── vagrant-spec.config.rb ├── test └── acceptance │ ├── skeletons │ ├── dns_dhcp_private │ │ └── Vagrantfile │ ├── dns_static_public │ │ └── Vagrantfile │ ├── tld_public_suffix │ │ └── Vagrantfile │ ├── dns_dhcp_private_callback │ │ └── Vagrantfile │ └── dns_static_private │ │ └── Vagrantfile │ └── dns │ ├── public_suffix_spec.rb │ ├── dhcp_private_callback_spec.rb │ ├── dhcp_private_spec.rb │ ├── static_public_spec.rb │ └── static_private_spec.rb ├── Gemfile ├── LICENSE ├── DEVELOPMENT.md ├── tasks └── acceptance.rake ├── vagrant-dns.gemspec ├── PLATFORM_SUPPORT.md ├── CHANGELOG.md └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.1.7 2 | -------------------------------------------------------------------------------- /lib/vagrant-dns/version.rb: -------------------------------------------------------------------------------- 1 | module VagrantDNS 2 | VERSION = "2.4.2" 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | task_dir = File.expand_path("../tasks", __FILE__) 5 | Dir["#{task_dir}/**/*.rake"].each do |task_file| 6 | load task_file 7 | end 8 | -------------------------------------------------------------------------------- /testdrive/bin/vagrant: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | TESTDRIVE=${VAGRANT_DEV:-$(dirname "$(cd "$(dirname "$0")" && pwd)")} 3 | 4 | VAGRANT_DEFAULT_PROVIDER=virtualbox \ 5 | VAGRANT_HOME=${TESTDRIVE}/.vagrant.d \ 6 | bundle exec vagrant $@ 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | test/acceptance/artifacts 18 | tmp 19 | testdrive/.vagrant 20 | Gemfile.local 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | 7 | [{*.rb,Gemfile,Vagrantfile}] 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /lib/vagrant-dns/installers.rb: -------------------------------------------------------------------------------- 1 | module VagrantDNS 2 | module Installers 3 | def self.resolve 4 | if Vagrant::Util::Platform.darwin? 5 | VagrantDNS::Installers::Mac 6 | elsif Vagrant::Util::Platform.linux? 7 | VagrantDNS::Installers::Linux 8 | else 9 | raise 'installing and uninstalling is only supported on Linux and macOS at the moment.' 10 | end 11 | end 12 | end 13 | end 14 | 15 | require "vagrant-dns/installers/mac" 16 | require "vagrant-dns/installers/linux" 17 | -------------------------------------------------------------------------------- /lib/vagrant-dns/middlewares/config_up.rb: -------------------------------------------------------------------------------- 1 | module VagrantDNS 2 | module Middlewares 3 | class ConfigUp 4 | def initialize(app, env) 5 | @app = app 6 | @env = env 7 | end 8 | 9 | def call(env) 10 | @env = env 11 | vm = env[:machine] 12 | if VagrantDNS::Config.auto_run 13 | tmp_path = File.join(vm.env.tmp_path, "dns") 14 | VagrantDNS::Configurator.new(vm, tmp_path).up! 15 | end 16 | @app.call(env) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/vagrant-dns/middlewares/config_down.rb: -------------------------------------------------------------------------------- 1 | module VagrantDNS 2 | module Middlewares 3 | class ConfigDown 4 | def initialize(app, env) 5 | @app = app 6 | @env = env 7 | end 8 | 9 | def call(env) 10 | @env = env 11 | vm = env[:machine] 12 | if VagrantDNS::Config.auto_run 13 | tmp_path = File.join(vm.env.tmp_path, "dns") 14 | VagrantDNS::Configurator.new(vm, tmp_path).down! 15 | end 16 | @app.call(env) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/vagrant-dns/middlewares/restart.rb: -------------------------------------------------------------------------------- 1 | module VagrantDNS 2 | module Middlewares 3 | class Restart 4 | def initialize(app, env) 5 | @app = app 6 | @env = env 7 | end 8 | 9 | def call(env) 10 | @env = env 11 | vm = env[:machine] 12 | if VagrantDNS::Config.auto_run 13 | tmp_path = File.join(vm.env.tmp_path, "dns") 14 | VagrantDNS::Service.new(tmp_path).restart! 15 | env[:ui].info "Restarted DNS Service" 16 | end 17 | @app.call(env) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /vagrant-spec.config.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require "vagrant-spec/acceptance" 3 | 4 | Vagrant::Spec::Acceptance.configure do |c| 5 | acceptance_dir = Pathname.new File.expand_path("../test/acceptance", __FILE__) 6 | 7 | c.component_paths = [acceptance_dir.to_s] 8 | c.skeleton_paths = [(acceptance_dir + 'skeletons').to_s] 9 | 10 | c.provider ENV['VS_PROVIDER'], box: ENV['VS_BOX_PATH'], skeleton_path: c.skeleton_paths 11 | 12 | # there seems no other way to set additional environment variables 13 | # see: https://github.com/mitchellh/vagrant-spec/pull/17 14 | c.instance_variable_set(:@env, c.env.merge('VBOX_USER_HOME' => "{{homedir}}")) 15 | end 16 | -------------------------------------------------------------------------------- /lib/vagrant-dns/registry.rb: -------------------------------------------------------------------------------- 1 | module VagrantDNS 2 | # This is the dns pattern registry (aka "config") 3 | # It basically delegates everything to a YAML::Store but handles the conversion 4 | # of Regexp dns-patterns into YAML string keys and reverse. 5 | class Registry 6 | include VagrantDNS::Store 7 | 8 | NAME = "config" 9 | 10 | def initialize(tmp_path) 11 | @store = YAML::Store.new(File.join(tmp_path, NAME), true) 12 | end 13 | 14 | # @return [VagrantDNS::Registry,nil] Eitehr an instance or +nil+ if cofig file does not exist. 15 | def self.open(tmp_path) 16 | new(tmp_path) if File.exist?(File.join(tmp_path, NAME)) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/vagrant-dns/tld_registry.rb: -------------------------------------------------------------------------------- 1 | module VagrantDNS 2 | # This is the dns pattern registry (aka "config") 3 | # It basically delegates everything to a YAML::Store but handles the conversion 4 | # of Regexp dns-patterns into YAML string keys and reverse. 5 | class TldRegistry 6 | include VagrantDNS::Store 7 | 8 | NAME = "ltds" 9 | 10 | def initialize(tmp_path) 11 | @store = YAML::Store.new(File.join(tmp_path, NAME), true) 12 | end 13 | 14 | # @return [VagrantDNS::Registry,nil] Eitehr an instance or +nil+ if cofig file does not exist. 15 | def self.open(tmp_path) 16 | new(tmp_path) if File.exist?(File.join(tmp_path, NAME)) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/acceptance/skeletons/dns_dhcp_private/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "box" 6 | config.vm.network :private_network, type: :dhcp 7 | 8 | # vagrant-specs will fail otherwise 9 | config.vm.provider "virtualbox" do |v| 10 | v.gui = true 11 | end 12 | 13 | # we don't need synced_folder, also they might lead to failures due 14 | # to guest additions mismatches 15 | config.vm.synced_folder ".", "/vagrant", disabled: true 16 | 17 | config.dns.tld = 'spec' 18 | config.dns.patterns = /^.*dhcp-private.testbox.spec$/ 19 | 20 | VagrantDNS::Config.listen = [[:udp, "127.0.0.1", 5333]] 21 | end 22 | -------------------------------------------------------------------------------- /test/acceptance/skeletons/dns_static_public/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "box" 6 | config.vm.network :public_network, ip: '10.10.10.102', bridge: "en0: Wi-Fi (AirPort)" 7 | 8 | # vagrant-specs will fail otherwise 9 | config.vm.provider "virtualbox" do |v| 10 | v.gui = true 11 | end 12 | 13 | # we don't need synced_folder, also they might lead to failures due 14 | # to guest additions mismatches 15 | config.vm.synced_folder ".", "/vagrant", disabled: true 16 | 17 | config.dns.tld = 'spec' 18 | config.dns.patterns = /^.*public.testbox.spec$/ 19 | 20 | VagrantDNS::Config.listen = [[:udp, "127.0.0.1", 5333]] 21 | end 22 | -------------------------------------------------------------------------------- /test/acceptance/skeletons/tld_public_suffix/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "box" 6 | config.vm.network :private_network, ip: '10.10.10.101' 7 | 8 | # vagrant-specs will fail otherwise 9 | config.vm.provider "virtualbox" do |v| 10 | v.gui = true 11 | end 12 | 13 | # we don't need synced_folder, also they might lead to failures due 14 | # to guest additions mismatches 15 | config.vm.synced_folder ".", "/vagrant", disabled: true 16 | 17 | config.dns.tld = 'com' 18 | config.dns.patterns = /^.*public.testbox.com$/ 19 | #---VagrantDNS::Config.check_public_suffix---# 20 | 21 | VagrantDNS::Config.listen = [[:udp, "127.0.0.1", 5333]] 22 | end 23 | -------------------------------------------------------------------------------- /test/acceptance/skeletons/dns_dhcp_private_callback/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |global_config| 5 | global_config.vm.define :plain_dhcp do |config| 6 | 7 | config.vm.box = "box" 8 | config.vm.network :private_network, type: :dhcp 9 | 10 | # we don't need synced_folder, also they might lead to failures due 11 | # to guest additions mismatches 12 | config.vm.synced_folder ".", "/vagrant", disabled: true 13 | 14 | config.dns.tld = 'spec' 15 | config.dns.patterns = /^.*plain-dhcp-private.testbox.spec$/ 16 | config.dns.ip = -> (vm, opts) do 17 | ip = nil 18 | vm.communicate.execute("hostname -I") { |type, data| ip = data.split.last if type == :stdout } 19 | ip 20 | end 21 | 22 | VagrantDNS::Config.listen = [[:udp, "127.0.0.1", 5333]] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby(File.read(File.expand_path('.ruby-version', __dir__))[/\d+\.\d+\.\d+.*/]) 3 | 4 | ENV['TEST_VAGRANT_VERSION'] ||= 'v2.3.4' 5 | 6 | # Using the :plugins group causes Vagrant to automagially load auto_network 7 | # during acceptance tests. 8 | group :plugins do 9 | gemspec 10 | # source "https://gems.hashicorp.com/" do 11 | # gem "vagrant-vmware-desktop" 12 | # end 13 | end 14 | 15 | group :test, :development do 16 | if ENV['TEST_VAGRANT_VERSION'] == 'HEAD' 17 | gem 'vagrant', :git => 'https://github.com/hashicorp/vagrant', :branch => 'main' 18 | else 19 | gem 'vagrant', :git => 'https://github.com/hashicorp/vagrant', :tag => ENV['TEST_VAGRANT_VERSION'] 20 | end 21 | gem 'rubydns', '~> 2.0.0' 22 | end 23 | 24 | group :test do 25 | gem 'vagrant-spec', :git => 'https://github.com/hashicorp/vagrant-spec', :branch => 'main' 26 | gem 'rake' 27 | end 28 | 29 | if File.exist? "#{__FILE__}.local" 30 | eval(File.read("#{__FILE__}.local"), binding) 31 | end 32 | -------------------------------------------------------------------------------- /test/acceptance/skeletons/dns_static_private/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "box" 6 | 7 | # skip custom subnet and mask by default, add for virtualbox 8 | # https://developer.hashicorp.com/vagrant/docs/providers/vmware/known-issues#creating-network-devices 9 | config.vm.network :private_network 10 | 11 | # vagrant-specs will fail otherwise 12 | config.vm.provider "virtualbox" do |v, override| 13 | override.vm.network :private_network, ip: '10.10.10.101' 14 | v.gui = true 15 | end 16 | 17 | config.vm.provider "vmware_desktop" do |v, override| 18 | override.vm.network :private_network, ip: '192.168.134.111' 19 | end 20 | 21 | # we don't need synced_folder, also they might lead to failures due 22 | # to guest additions mismatches 23 | config.vm.synced_folder ".", "/vagrant", disabled: true 24 | 25 | config.dns.tld = 'spec' 26 | config.dns.patterns = /^.*private.testbox.spec$/ 27 | 28 | VagrantDNS::Config.listen = [[:udp, "127.0.0.1", 5333]] 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Florian Gilcher 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/vagrant-dns.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-dns/version" 2 | require "vagrant-dns/config" 3 | 4 | require "vagrant-dns/store" 5 | require "vagrant-dns/registry" 6 | require "vagrant-dns/tld_registry" 7 | require "vagrant-dns/service" 8 | require "vagrant-dns/installers" 9 | require "vagrant-dns/configurator" 10 | require "vagrant-dns/middlewares/config_up" 11 | require "vagrant-dns/middlewares/config_down" 12 | require "vagrant-dns/middlewares/restart" 13 | 14 | module VagrantDNS 15 | class Plugin < Vagrant.plugin("2") 16 | name "vagrant-dns" 17 | 18 | config "dns" do 19 | Config 20 | end 21 | 22 | command "dns" do 23 | require File.expand_path("../vagrant-dns/command", __FILE__) 24 | Command 25 | end 26 | 27 | %w{machine_action_up machine_action_reload}.each do |action| 28 | action_hook("restart_vagarant_dns_on_#{action}", action) do |hook, *args| 29 | hook_once VagrantDNS::Middlewares::ConfigUp, hook 30 | hook_once VagrantDNS::Middlewares::Restart, hook 31 | end 32 | end 33 | 34 | action_hook("remove_vagrant_dns_config", "machine_action_destroy") do |hook| 35 | hook_once VagrantDNS::Middlewares::ConfigDown, hook 36 | hook_once VagrantDNS::Middlewares::Restart, hook 37 | end 38 | 39 | # @private 40 | def self.hook_once(middleware, hook) 41 | return if hook.append_hooks.any? { |stack_item| 42 | if stack_item.is_a?(Array) 43 | stack_item.first == middleware 44 | else 45 | stack_item.middleware == middleware 46 | end 47 | 48 | } 49 | hook.append middleware 50 | end 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development Guidelines 2 | 3 | TODO: Expand this section :) 4 | 5 | The `Gemfile` uses the `TEST_VAGRANT_VERSION` environment variable to control which version of vagrant is used during local development. 6 | To adjust the ruby version used, change the contents of the `.ruby-version` file, even if you don't use a ruby version manager. 7 | 8 | ## Running Acceptance Tests 9 | 10 | This project uses `vagrant-spec` to run acceptance tests. 11 | See `test/acceptance/dns` for spec files and `test/acceptance/skeletons/*` for Vagrantfiles. 12 | See `tasks/acceptance.rake` for the basic setup of vagrant-spec. This file also defines which base box is used. 13 | Specs will take quite some time and run in GUI mode (due to some weirdness either in vagrant-spec and/or VirtualBox) which will render your machine basically unusable. 14 | 15 | ```bash 16 | # List available tasks: 17 | bundle exec rake -D acceptance 18 | 19 | # Setup specs (download box artifacts) 20 | bundle exec rake acceptance:setup:virtualbox 21 | 22 | # Run tests 23 | bundle exec rake acceptance:virtualbox 24 | ``` 25 | 26 | ### On other providers 27 | 28 | 1. If it's not a default provider, add it to to `Gemfile` as a dependency and re-run `bundle install`. 29 | 2. Add a box provider name to box URL mapping to the `TEST_BOXES` hash in `tasks/acceptance.rake` 30 | 3. Run the `acceptance` rake task like above, replacing `virtualbox` with your provider. 31 | 32 | 33 | ### VMware IPs 34 | 35 | VMware uses IPs from its bridges for DHCP and static private networks. Check IP and net masks using: 36 | 37 | ```console 38 | ifconfig -X bridge | grep 'inet ' | grep -v '127.0.0.1' | awk '{ print $2 "\t" $4 }' 39 | ``` 40 | -------------------------------------------------------------------------------- /tasks/acceptance.rake: -------------------------------------------------------------------------------- 1 | require "tmpdir" 2 | 3 | namespace :acceptance do 4 | def tmp_dir_path 5 | @tmp_dir_path ||= ENV["VS_TEMP"] || Dir.mktmpdir('vagrant-dns-spec') 6 | end 7 | 8 | ARTIFACT_DIR = File.join('test', 'acceptance', 'artifacts') 9 | 10 | TEST_BOXES = { 11 | # Ubuntu 16.04 https://app.vagrantup.com/ubuntu/boxes/xenial64.json 12 | # :virtualbox => "https://vagrantcloud.com/ubuntu/boxes/xenial64/versions/20180511.0.0/providers/virtualbox.box" 13 | # Ubuntu 18.04 https://app.vagrantup.com/ubuntu/boxes/bionic64.json 14 | virtualbox: "https://vagrantcloud.com/ubuntu/boxes/bionic64/versions/20180709.0.0/providers/virtualbox.box", 15 | # Ubuntu 22.10 https://app.vagrantup.com/bento/boxes/ubuntu-22.10 16 | vmware_desktop: "https://app.vagrantup.com/bento/boxes/ubuntu-22.10/versions/202303.13.0/providers/vmware_desktop.box" 17 | } 18 | 19 | TEST_BOXES.each do |provider, box_url| 20 | # Declare file download tasks 21 | directory ARTIFACT_DIR 22 | 23 | file File.join(ARTIFACT_DIR, "#{provider}.box") => ARTIFACT_DIR do |path| 24 | puts 'Downloading: ' + box_url 25 | Kernel.system 'curl', '-L', '-o', path.to_s, box_url 26 | end 27 | 28 | desc "Run acceptance tests for #{provider}" 29 | task provider => :"setup:#{provider}" do |task| 30 | box_path = File.expand_path(File.join('..', '..', ARTIFACT_DIR, "#{provider}.box"), __FILE__) 31 | puts "TMPDIR: #{tmp_dir_path}" 32 | Kernel.system( 33 | { 34 | "VS_PROVIDER" => provider.to_s, 35 | "VS_BOX_PATH" => box_path, 36 | "TMPDIR" => tmp_dir_path 37 | }, 38 | "bundle", "exec", "vagrant-spec", "test" 39 | ) 40 | end 41 | 42 | desc "downloads test boxes and other artifacts for #{provider}" 43 | task :"setup:#{provider}" => File.join(ARTIFACT_DIR, "#{provider}.box") 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /vagrant-dns.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/vagrant-dns/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Florian Gilcher", "Robert Schulze"] 6 | gem.email = ["florian.gilcher@asquera.de", "robert@dotless.de"] 7 | gem.description = %q{vagrant-dns is a vagrant plugin that manages DNS records associated with local machines.} 8 | gem.summary = %q{vagrant-dns manages DNS records of vagrant machines} 9 | gem.homepage = "https://github.com/BerlinVagrant/vagrant-dns" 10 | gem.license = "MIT" 11 | 12 | gem.files = `git ls-files`.split($\).reject { |f| f.match(%r{^(test|testdrive)/}) } 13 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 14 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 15 | gem.name = "vagrant-dns" 16 | gem.require_paths = ["lib"] 17 | gem.version = VagrantDNS::VERSION 18 | 19 | gem.metadata = { 20 | "bug_tracker_uri" => "https://github.com/BerlinVagrant/vagrant-dns/issues", 21 | "changelog_uri" => "https://github.com/BerlinVagrant/vagrant-dns/blob/master/CHANGELOG.md", 22 | "source_code_uri" => "https://github.com/BerlinVagrant/vagrant-dns", 23 | "wiki_uri" => "https://github.com/BerlinVagrant/vagrant-dns/wiki" 24 | } 25 | 26 | gem.required_ruby_version = '>= 2.2.6' 27 | 28 | gem.add_dependency "daemons" 29 | gem.add_dependency "rubydns", '~> 2.0.0' 30 | gem.add_dependency "async-dns", '< 1.4.0' 31 | gem.add_dependency "public_suffix" 32 | 33 | # Pinning async gem to work around an issue in vagrant, where it does not 34 | # honor "required_ruby_version" while resolving sub-dependencies. 35 | # see: https://github.com/hashicorp/vagrant/issues/12640 36 | gem.add_dependency 'async', '~> 1.30', '>= 1.30.3' 37 | 38 | gem.add_development_dependency 'rspec' 39 | end 40 | -------------------------------------------------------------------------------- /testdrive/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | # All Vagrant configuration is done here. The most common configuration 6 | # options are documented and commented below. For a complete reference, 7 | # please see the online documentation at vagrantup.com. 8 | 9 | # Every Vagrant virtual environment requires a box to build off of. 10 | # config.vm.box = 'hashicorp/bionic64' 11 | config.vm.box = "bento/ubuntu-22.10" 12 | # config.vm.box_url = 'http://files.vagrantup.com/precise32.box' 13 | 14 | config.vm.synced_folder ".", "/vagrant", disabled: true 15 | 16 | config.vm.hostname = "machine" 17 | # config.vm.network :private_network, ip: "33.33.33.60" 18 | config.vm.network :private_network, type: :dhcp 19 | # config.vm.network :public_network, ip: "192.168.178.222", bridge: "en0: Wi-Fi (AirPort)" 20 | # config.vm.network :public_network, use_dhcp_assigned_default_route: true, bridge: "en0: Wi-Fi (AirPort)" 21 | 22 | config.dns.tld = "test" 23 | config.dns.patterns = /^.*machine.test$/ 24 | # config.dns.ip = -> (vm, opts) do 25 | # ip = nil 26 | # vm.communicate.execute("hostname -I | cut -d ' ' -f 1") do |type, data| 27 | # ip = data.strip if type == :stdout 28 | # end 29 | # ip 30 | # end 31 | 32 | VagrantDNS::Config.listen = [[:udp, "127.0.0.1", 5300]] 33 | VagrantDNS::Config.ttl = 30 34 | 35 | VagrantDNS::Config.passthrough = false # :unknown 36 | VagrantDNS::Config.passthrough_resolver = [ [:udp, "1.0.0.1", 53], ["tcp", "1.0.0.1", 53] ] 37 | 38 | (1..3).each do |i| 39 | config.vm.define "worker#{i}" do |subconfig| 40 | subconfig.dns.tld = "test" 41 | subconfig.vm.hostname = "vagrant#{i}" 42 | subconfig.dns.patterns = "worker#{i}.mysite.test" 43 | subconfig.vm.box = "bento/ubuntu-22.10" 44 | config.vm.network :private_network, type: :dhcp 45 | # subconfig.vm.network "private_network", ip: "10.240.0.#{i+15}" 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/vagrant-dns/store.rb: -------------------------------------------------------------------------------- 1 | require 'yaml/store' 2 | require 'forwardable' 3 | 4 | module VagrantDNS 5 | module Store 6 | module InstanceMethods 7 | # This method is only valid in a #transaction and it cannot be read-only. It will raise PStore::Error if called at any other time. 8 | def [](name) 9 | name = name.source if name.respond_to? :source 10 | @store[name] 11 | end 12 | 13 | # This method is only valid in a #transaction and it cannot be read-only. It will raise PStore::Error if called at any other time. 14 | def []=(name, value) 15 | name = name.source if name.respond_to? :source 16 | @store[name] = value 17 | end 18 | 19 | def delete(name) 20 | name = name.source if name.respond_to? :source 21 | @store.delete(name) 22 | end 23 | 24 | # This method is only valid in a #transaction and it cannot be read-only. It will raise PStore::Error if called at any other time. 25 | def root?(name) 26 | name = name.source if name.respond_to? :source 27 | @store.root?(name) 28 | end 29 | alias_method :key?, :root? 30 | 31 | # This method is only valid in a #transaction and it cannot be read-only. It will raise PStore::Error if called at any other time. 32 | def roots 33 | @store.roots.map { |name| Regexp.new(name) } 34 | end 35 | alias_method :keys, :roots 36 | 37 | # This method is only valid in a #transaction and it cannot be read-only. It will raise PStore::Error if called at any other time. 38 | def any? 39 | @store.roots.any? 40 | end 41 | 42 | def to_hash 43 | @store.transaction(true) do 44 | @store.roots.each_with_object({}) do |name, a| 45 | a[Regexp.new(name)] = @store[name] 46 | end 47 | end 48 | end 49 | end 50 | 51 | def self.included(receiver) 52 | receiver.class_eval do 53 | extend Forwardable 54 | def_delegators :@store, :transaction, :abort, :commit 55 | include InstanceMethods 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/vagrant-dns/installers/linux.rb: -------------------------------------------------------------------------------- 1 | module VagrantDNS 2 | module Installers 3 | class Linux 4 | EXEC_STYLES = %i{sudo} 5 | RESOLVED_CONFIG = "vagrant-dns.conf" 6 | 7 | attr_accessor :tmp_path, :install_path, :exec_style 8 | 9 | def initialize(tmp_path, options = {}) 10 | self.tmp_path = tmp_path 11 | self.install_path = options.fetch(:install_path, "/etc/systemd/resolved.conf.d") 12 | self.exec_style = options.fetch(:exec_style, :sudo) 13 | end 14 | 15 | # Generate the resolved config. 16 | # 17 | # @yield [filename, content] 18 | # @yieldparam filename [String] The filename to use for the resolved config file (relative to +install_path+) 19 | # @yieldparam content [String] The content for +filename+ 20 | def self.resolver_files(ip, port, tlds) 21 | contents = 22 | "# This file is generated by vagrant-dns\n" \ 23 | "[Resolve]\n" \ 24 | "DNS=#{ip}:#{port}\n" \ 25 | "Domains=~#{tlds.join(' ~')}\n" 26 | 27 | yield "vagrant-dns.conf", contents 28 | end 29 | 30 | def install! 31 | require 'fileutils' 32 | 33 | src = File.join(tmp_path, "resolver", RESOLVED_CONFIG) 34 | dest = File.join(install_path, RESOLVED_CONFIG) 35 | 36 | commands = [ 37 | ['install', '-D', '-m', '0644', '-T', src.shellescape, dest.shellescape], 38 | ['systemctl', 'reload-or-restart', 'systemd-resolved.service'] 39 | ] 40 | 41 | exec(*commands) 42 | end 43 | 44 | def uninstall! 45 | commands = [ 46 | ['rm', File.join(install_path, RESOLVED_CONFIG)], 47 | ['systemctl', 'reload-or-restart', 'systemd-resolved.service'] 48 | ] 49 | 50 | exec(*commands) 51 | end 52 | 53 | def purge! 54 | require 'fileutils' 55 | uninstall! 56 | FileUtils.rm_r(tmp_path) 57 | end 58 | 59 | def exec(*commands) 60 | return if !commands || commands.empty? 61 | 62 | case exec_style 63 | when :sudo 64 | commands.each do |c| 65 | system 'sudo', *c 66 | end 67 | else 68 | raise ArgumentError, "Unsupported execution style: #{exec_style}. Use one of #{EXEC_STYLES.map(&:inspect).join(' ')}" 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/vagrant-dns/service.rb: -------------------------------------------------------------------------------- 1 | require 'daemons' 2 | 3 | module VagrantDNS 4 | class Service 5 | attr_accessor :tmp_path 6 | 7 | def initialize(tmp_path) 8 | self.tmp_path = tmp_path 9 | end 10 | 11 | def start!(opts = {}) 12 | run!("start", { ontop: opts[:ontop] }) 13 | end 14 | 15 | def stop! 16 | run!("stop") 17 | end 18 | 19 | def status! 20 | run!("status") 21 | end 22 | 23 | def run!(cmd, opts = {}) 24 | # On darwin, when the running Ruby is not compiled for the running OS 25 | # @see: https://github.com/BerlinVagrant/vagrant-dns/issues/72 26 | use_issue_72_workround = RUBY_PLATFORM.match?(/darwin/) && !RUBY_PLATFORM.end_with?(`uname -r`[0, 2]) 27 | 28 | if cmd == "start" && use_issue_72_workround 29 | require_relative "./server" 30 | end 31 | 32 | Daemons.run_proc("vagrant-dns", run_options(cmd, opts)) do 33 | unless use_issue_72_workround 34 | require_relative "./server" 35 | end 36 | 37 | VagrantDNS::Server.new( 38 | Registry.new(tmp_path), 39 | listen: VagrantDNS::Config.listen, 40 | ttl: VagrantDNS::Config.ttl, 41 | passthrough: VagrantDNS::Config.passthrough, 42 | resolver: VagrantDNS::Config.passthrough_resolver 43 | ).run 44 | end 45 | end 46 | 47 | def restart!(start_opts = {}) 48 | stop! 49 | start!(start_opts) 50 | end 51 | 52 | def show_config 53 | registry = Registry.open(tmp_path) 54 | return unless registry 55 | 56 | config = registry.to_hash 57 | if config.any? 58 | config.each do |pattern, ip| 59 | puts format("%s => %s", pattern.inspect, ip) 60 | end 61 | else 62 | puts "Pattern configuration missing or empty." 63 | end 64 | end 65 | 66 | def show_tld_config 67 | tld_registry = VagrantDNS::TldRegistry.open(tmp_path) 68 | return unless tld_registry 69 | 70 | tlds = tld_registry.transaction { |store| store.fetch("tlds", []) } 71 | if !tlds || tlds.empty? 72 | puts "No TLDs configured." 73 | else 74 | puts tlds 75 | end 76 | end 77 | 78 | private def run_options(cmd, extra = {}) 79 | daemon_dir = File.join(tmp_path, "daemon") 80 | { 81 | ARGV: [cmd], 82 | dir_mode: :normal, 83 | dir: daemon_dir, 84 | log_output: true, 85 | log_dir: daemon_dir, 86 | **extra 87 | } 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/vagrant-dns/installers/mac.rb: -------------------------------------------------------------------------------- 1 | module VagrantDNS 2 | module Installers 3 | class Mac 4 | EXEC_STYLES = %i{osa sudo} 5 | 6 | attr_accessor :tmp_path, :install_path, :exec_style 7 | 8 | def initialize(tmp_path, options = {}) 9 | self.tmp_path = tmp_path 10 | self.install_path = options.fetch(:install_path, "/etc/resolver") 11 | self.exec_style = options.fetch(:exec_style, :osa) 12 | end 13 | 14 | # Generate the resolved config. 15 | # 16 | # @yield [filename, content] 17 | # @yieldparam filename [String] The filename to use for the resolved config file (relative to +install_path+) 18 | # @yieldparam content [String] The content for +filename+ 19 | def self.resolver_files(ip, port, tlds) 20 | tlds.each do |tld| 21 | contents = 22 | "# This file is generated by vagrant-dns\n" \ 23 | "nameserver #{ip}\n" \ 24 | "port #{port}" 25 | 26 | yield tld, contents 27 | end 28 | end 29 | 30 | def install! 31 | require 'fileutils' 32 | 33 | commands = [ 34 | ['mkdir', '-p', install_path] 35 | ] 36 | 37 | commands += registered_resolvers.map do |resolver| 38 | ['ln', '-sf', resolver.shellescape, install_path.shellescape] 39 | end 40 | 41 | exec(*commands) 42 | end 43 | 44 | def uninstall! 45 | commands = registered_resolvers.map do |r| 46 | installed_resolver = File.join(install_path, File.basename(r)) 47 | ['rm', '-rf', installed_resolver] 48 | end 49 | 50 | exec(*commands) 51 | end 52 | 53 | def purge! 54 | require 'fileutils' 55 | uninstall! 56 | FileUtils.rm_r(tmp_path) 57 | end 58 | 59 | def registered_resolvers 60 | Dir[File.join(tmp_path, "resolver", "*")] 61 | end 62 | 63 | def exec(*commands) 64 | return if !commands || commands.empty? 65 | 66 | case exec_style 67 | when :osa 68 | cmd_script = commands.map {|line| line.join ' ' }.join($/) 69 | system 'osascript', '-e', "do shell script \"#{cmd_script}\" with administrator privileges" 70 | when :sudo 71 | commands.each do |c| 72 | system 'sudo', *c 73 | end 74 | else 75 | raise ArgumentError, "Unsupported execution style: #{exec_style}. Use one of #{EXEC_STYLES.map(&:inspect).join(' ')}" 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/vagrant-dns/server.rb: -------------------------------------------------------------------------------- 1 | require 'pp' 2 | require 'rubydns' 3 | require 'async/dns/system' 4 | 5 | module VagrantDNS 6 | class Server 7 | attr_reader :registry, :listen, :ttl, :resolver, :passthrough 8 | 9 | def initialize(registry, listen:, ttl:, resolver:, passthrough:) 10 | @registry = registry.to_hash 11 | @listen = listen 12 | @ttl = ttl 13 | 14 | @resolver = if resolver.nil? || resolver == :system 15 | RubyDNS::Resolver.new(Async::DNS::System.nameservers) 16 | elsif !resolver || resolver.empty? 17 | nil 18 | else 19 | RubyDNS::Resolver.new(resolver) 20 | end 21 | 22 | if passthrough && !resolver 23 | puts "[Warning] 'passthrough' config has no effect, sice no passthrough resolver is set." 24 | end 25 | 26 | @passthrough = !!@resolver && passthrough 27 | end 28 | 29 | def run 30 | # need those clusures for the +RubyDNS::run_server+ block 31 | passthrough = self.passthrough 32 | registry = self.registry 33 | resolver = self.resolver 34 | ttl = self.ttl 35 | 36 | _passthrough = if passthrough 37 | proc do |transaction| 38 | transaction.passthrough!(resolver) do |response| 39 | puts "Passthrough response: #{response.inspect}" 40 | end 41 | end 42 | end 43 | 44 | RubyDNS::run_server(listen) do 45 | # match all known patterns first 46 | registry.each do |pattern, ip| 47 | match(pattern, Resolv::DNS::Resource::IN::A) do |transaction, match_data| 48 | transaction.respond!(ip, ttl: ttl) 49 | end 50 | end 51 | 52 | case passthrough 53 | when true 54 | # forward everything 55 | otherwise(&_passthrough) 56 | when false 57 | # fail known patterns for non-A queries as NotImp 58 | registry.each do |pattern, ip| 59 | match(pattern) do |transaction, match_data| 60 | transaction.fail!(:NotImp) 61 | end 62 | end 63 | 64 | # unknown pattern end up as NXDomain 65 | otherwise do |transaction| 66 | transaction.fail!(:NXDomain) 67 | end 68 | when :unknown 69 | # fail known patterns for non-A queries as NotImp 70 | registry.each do |pattern, ip| 71 | match(pattern) do |transaction, match_data| 72 | transaction.fail!(:NotImp) 73 | end 74 | end 75 | 76 | # forward only unknown patterns 77 | otherwise(&_passthrough) 78 | end 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/vagrant-dns/config.rb: -------------------------------------------------------------------------------- 1 | require "logger" 2 | 3 | module VagrantDNS 4 | class Config < Vagrant.plugin(2, :config) 5 | class << self 6 | attr_writer :listen, :ttl, :passthrough, :passthrough_resolver 7 | attr_accessor :logger, :auto_run, :check_public_suffix 8 | 9 | def listen 10 | @listen ||= [[:udp, "127.0.0.1", 5300]] 11 | end 12 | 13 | def ttl 14 | @ttl ||= 300 15 | end 16 | 17 | def passthrough 18 | return true if @passthrough.nil? 19 | @passthrough 20 | end 21 | 22 | def passthrough_resolver 23 | @passthrough_resolver ||= :system 24 | end 25 | 26 | def auto_run 27 | return true if @auto_run.nil? 28 | @auto_run 29 | end 30 | 31 | def check_public_suffix 32 | return "warn" if @check_public_suffix.nil? 33 | @check_public_suffix 34 | end 35 | 36 | def validate_tlds(vm) 37 | return [true, nil] unless check_public_suffix 38 | 39 | require "public_suffix" 40 | 41 | # Don't include private domains in the list, we are only interested in TLDs and such 42 | list = PublicSuffix::List.parse( 43 | File.read(PublicSuffix::List::DEFAULT_LIST_PATH), 44 | private_domains: false 45 | ) 46 | 47 | failed_tlds = vm.config.dns.tlds.select { |tld| list.find(tld, default: false) } 48 | 49 | err = if failed_tlds.any? 50 | "tlds include a public suffix: #{failed_tlds.join(", ")}" 51 | end 52 | 53 | if err && check_public_suffix.to_s == "error" 54 | [false, err] 55 | elsif err 56 | [true, err] 57 | else 58 | [true, nil] 59 | end 60 | end 61 | end 62 | 63 | attr_accessor :records, :ip 64 | attr_reader :tlds, :patterns 65 | 66 | def pattern=(pattern) 67 | @patterns = (pattern ? Array(pattern) : pattern) 68 | end 69 | alias_method :patterns=, :pattern= 70 | 71 | def tld=(tld) 72 | @tlds = Array(tld) 73 | end 74 | 75 | def tlds 76 | @tlds ||= [] 77 | end 78 | 79 | # explicit hash, to get symbols in hash keys 80 | def to_hash 81 | { patterns: patterns, records: records, tlds: tlds, ip: ip } 82 | end 83 | 84 | def validate(machine) 85 | errors = _detected_errors 86 | 87 | errors = validate_check_public_suffix(errors, machine) 88 | 89 | { "vagrant_dns" => errors } 90 | end 91 | 92 | def validate_check_public_suffix(errors, machine) 93 | valid, err = self.class.validate_tlds(machine) 94 | 95 | if !valid 96 | errors << err 97 | elsif err 98 | machine.ui.warn(err) 99 | end 100 | 101 | errors 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/acceptance/dns/public_suffix_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'provider/tld_public_suffix' do |provider, options| 2 | 3 | if options[:box] && !File.file?(options[:box]) 4 | raise ArgumentError, 5 | "A box file #{options[:box]} must be downloaded for provider: #{provider}. Try: rake acceptance:setup" 6 | end 7 | 8 | include_context 'acceptance' 9 | let(:tmp_path) { environment.homedir } 10 | 11 | let(:tld) { 'com' } 12 | let(:name) { 'public.testbox.com' } 13 | 14 | let(:error_message) { "tlds include a public suffix: #{tld}" } 15 | 16 | before do 17 | ENV['VAGRANT_DEFAULT_PROVIDER'] = provider 18 | environment.skeleton('tld_public_suffix') 19 | 20 | vagrantfile = environment.workdir.join("Vagrantfile") 21 | new_conent = File.read(vagrantfile).gsub(/^\s*(#---VagrantDNS::Config.check_public_suffix---#)/, check_public_suffix_line) 22 | File.write(vagrantfile, new_conent) 23 | end 24 | 25 | after do 26 | # ensure we don't mess up our config 27 | execute("sudo", "rm", "-f", "/etc/resolver/#{tld}", log: false) 28 | end 29 | 30 | describe "default config warns" do 31 | let(:check_public_suffix_line) { "" } 32 | 33 | it "validates and prints error" do 34 | result = execute('vagrant', 'validate') 35 | expect(result).to exit_with(0) 36 | expect(result.stderr).to be_empty 37 | end 38 | end 39 | 40 | describe "config level 'error'" do 41 | let(:check_public_suffix_line) do 42 | <<-RUBY 43 | VagrantDNS::Config.check_public_suffix = "error" 44 | RUBY 45 | end 46 | 47 | it "fails validation and prints error" do 48 | result = execute('vagrant', 'validate') 49 | expect(result).to_not exit_with(0) 50 | expect(result.stderr).to include(error_message) 51 | end 52 | 53 | it "will not start the box" do 54 | assert_execute('vagrant', 'box', 'add', 'box', options[:box]) 55 | 56 | result_up = execute('vagrant', 'up', "--provider=#{provider}") 57 | expect(result_up.stderr).to include(error_message) 58 | expect(result_up).to_not exit_with(0) 59 | 60 | result_st = execute('vagrant', 'status') 61 | expect(result_st.stdout).to match(/default\s+not created/) 62 | end 63 | 64 | it "does not register a resolver" do 65 | result = execute('vagrant', 'dns', '--install', '--with-sudo') 66 | expect(result.stderr).to include(error_message) 67 | expect(execute('sudo', 'test', '-f', "/etc/resolver/#{tld}")).to_not exit_with(0) 68 | end 69 | end 70 | 71 | describe "config `false`" do 72 | let(:check_public_suffix_line) do 73 | <<-RUBY 74 | VagrantDNS::Config.check_public_suffix = false 75 | RUBY 76 | end 77 | 78 | it "validates and prints error" do 79 | result = execute('vagrant', 'validate') 80 | expect(result).to exit_with(0) 81 | expect(result.stderr).to be_empty 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/acceptance/dns/dhcp_private_callback_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'provider/dns_dhcp_private_callback' do |provider, options| 2 | 3 | if options[:box] && !File.file?(options[:box]) 4 | raise ArgumentError, 5 | "A box file #{options[:box]} must be downloaded for provider: #{provider}. Try: rake acceptance:setup" 6 | end 7 | 8 | include_context 'acceptance' 9 | let(:tmp_path) { environment.instance_variable_get(:@homedir) } 10 | 11 | let(:box_ip) { /(172|192).\d+.\d+.\d+/ } # that's some default dhcp range here 12 | let(:tld) { 'spec' } 13 | let(:name) { 'plain-dhcp-private.testbox.spec' } 14 | 15 | before do 16 | ENV['VAGRANT_DEFAULT_PROVIDER'] = provider 17 | environment.skeleton('dns_dhcp_private_callback') 18 | end 19 | 20 | describe 'installation' do 21 | it 'creates and removes resolver link with logged warning that no IP could be found' do 22 | result = assert_execute('vagrant', 'dns', '--install', '--with-sudo') 23 | expect(result.stdout).to include("[vagrant-dns] Postponing running user provided IP script until box has started.") 24 | expect(result.stdout).to include("[vagrant-dns] No patterns will be configured.") 25 | 26 | assert_execute('sudo', 'test', '-f', "/etc/resolver/#{tld}") 27 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 28 | assert_execute('sudo', 'test', '!', '-f', "/etc/resolver/#{tld}") 29 | end 30 | 31 | it 'skips creating config file' do 32 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 33 | assert_execute('sudo', 'test', '!', '-f', "#{tmp_path}/tmp/dns/config") 34 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 35 | end 36 | end 37 | 38 | describe 'running' do 39 | before do 40 | assert_execute('vagrant', 'box', 'add', 'box', options[:box]) 41 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 42 | assert_execute('vagrant', 'up', "--provider=#{provider}") 43 | end 44 | 45 | after do 46 | # Ensure any VMs that survived tests are cleaned up. 47 | execute('vagrant', 'destroy', '--force', log: false) 48 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 49 | end 50 | 51 | it 'auto-starts the DNS daemon' do 52 | assert_execute('pgrep', '-lf', 'vagrant-dns') 53 | end 54 | 55 | it 'registered as a resolver' do 56 | # the output of `scutil` changes it's format over MacOS versions a bit 57 | expected_output = Regexp.new(<<-TXT.gsub(/^\s*/, ''), Regexp::MULTILINE) 58 | \\s*domain\\s*: #{tld} 59 | \\s*nameserver\\[0\\]\\s*: 127.0.0.1 60 | \\s*port\\s*: 5333 61 | \\s*flags\\s*: Request A records, Request AAAA records 62 | \\s*reach\\s*:(?=.*\\bReachable\\b)(?=.*\\bLocal Address\\b).* 63 | TXT 64 | 65 | result = assert_execute('scutil', '--dns') 66 | expect(result.stdout).to match(expected_output) 67 | end 68 | 69 | it 'responds to host-names' do 70 | result = assert_execute('dscacheutil', '-q', 'host', '-a', 'name', "#{name}") 71 | expect(result.stdout).to match(/ip_address: #{box_ip}/) 72 | 73 | result = assert_execute('dscacheutil', '-q', 'host', '-a', 'name', "www.#{name}") 74 | expect(result.stdout).to match(/ip_address: #{box_ip}/) 75 | 76 | result = execute('dscacheutil', '-q', 'host', '-a', 'name', "notthere.#{tld}") 77 | expect(result.stdout).to_not match(/ip_address: #{box_ip}/) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/acceptance/dns/dhcp_private_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'provider/dns_dhcp_private' do |provider, options| 2 | 3 | if options[:box] && !File.file?(options[:box]) 4 | raise ArgumentError, 5 | "A box file #{options[:box]} must be downloaded for provider: #{provider}. Try: rake acceptance:setup" 6 | end 7 | 8 | include_context 'acceptance' 9 | let(:tmp_path) { environment.homedir } 10 | 11 | let(:box_ip) { /(172|192).\d+.\d+.\d+/ } # that's some default dhcp range here 12 | let(:tld) { 'spec' } 13 | let(:name) { 'dhcp-private.testbox.spec' } 14 | 15 | before do 16 | ENV['VAGRANT_DEFAULT_PROVIDER'] = provider 17 | environment.skeleton('dns_dhcp_private') 18 | end 19 | 20 | describe 'installation' do 21 | it 'creates and removes resolver link with logged warning that no IP could be found' do 22 | result = assert_execute('vagrant', 'dns', '--install', '--with-sudo') 23 | expect(result.stdout).to include("[vagrant-dns] Postponing running user provided IP script until box has started.") 24 | expect(result.stdout).to include("[vagrant-dns] No patterns will be configured.") 25 | 26 | assert_execute('sudo', 'test', '-f', "/etc/resolver/#{tld}") 27 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 28 | assert_execute('sudo', 'test', '!', '-f', "/etc/resolver/#{tld}") 29 | end 30 | 31 | it 'skips creating config file' do 32 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 33 | assert_execute('sudo', 'test', '!', '-f', "#{tmp_path}/tmp/dns/config") 34 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 35 | end 36 | end 37 | 38 | describe 'running' do 39 | before do 40 | assert_execute('vagrant', 'box', 'add', 'box', options[:box]) 41 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 42 | assert_execute('vagrant', 'up', "--provider=#{provider}") 43 | end 44 | 45 | after do 46 | # Ensure any VMs that survived tests are cleaned up. 47 | execute('vagrant', 'destroy', '--force', log: false) 48 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 49 | end 50 | 51 | it 'auto-starts the DNS daemon' do 52 | assert_execute('pgrep', '-lf', 'vagrant-dns') 53 | end 54 | 55 | it 'registered as a resolver' do 56 | # the output of `scutil` changes it's format over MacOS versions a bit 57 | expected_output = Regexp.new(<<-TXT.gsub(/^\s*/, ''), Regexp::MULTILINE) 58 | \\s*domain\\s*: #{tld} 59 | \\s*nameserver\\[0\\]\\s*: 127.0.0.1 60 | \\s*port\\s*: 5333 61 | \\s*flags\\s*: Request A records, Request AAAA records 62 | \\s*reach\\s*:(?=.*\\bReachable\\b)(?=.*\\bLocal Address\\b).* 63 | TXT 64 | 65 | result = assert_execute('scutil', '--dns') 66 | expect(result.stdout).to match(expected_output) 67 | end 68 | 69 | it 'responds to host-names' do 70 | result = assert_execute('dscacheutil', '-q', 'host', '-a', 'name', "#{name}") 71 | expect(result.stdout).to match(/ip_address: #{box_ip}/) 72 | 73 | result = assert_execute('dscacheutil', '-q', 'host', '-a', 'name', "www.#{name}") 74 | expect(result.stdout).to match(/ip_address: #{box_ip}/) 75 | 76 | result = execute('dscacheutil', '-q', 'host', '-a', 'name', "notthere.#{tld}") 77 | expect(result.stdout).to_not match(/ip_address: #{box_ip}/) 78 | end 79 | 80 | it 'vagrant box starts up and is usable' do 81 | result = assert_execute('vagrant', 'ssh', '-c', 'whoami') 82 | expect(result.stdout).to include("vagrant") 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /PLATFORM_SUPPORT.md: -------------------------------------------------------------------------------- 1 | Platform Support 2 | ================ 3 | 4 | Vagrant-DNS was originally developed for macOS (or Mac OS X back in the time). This had 2 main reasons: 5 | 6 | 1) All main developers are on macOS and have no good idea how to properly implement such a system for other OSes. Also, macOS makes it incredibly easy to do what vagrant-dns does. 7 | 8 | 2) We spoke to multiple Linux and Windows-Developers and found that a half-baked implementation of people not invested at the respective platform as a _convenient development_ platform is only harmful and might at worst mess with users systems in unintended ways. None of them have supplied us with a proper way to implement this. 9 | 10 | Vagrant-DNS just recently gained support for Linux, more specificity systemd with resolved. 11 | 12 | That said, we will happily accept any patches and want to encourage platform support beyond macOS, including ensuring further development of those solutions. Some of the groundwork for this has been layed. It might not be 100% finished, but we happily apply any changes necessary. 13 | 14 | This document aims to be a guide to implementing support for other platforms. 15 | 16 | How Vagrant-DNS currently works 17 | =============================== 18 | 19 | In a nutshell, Vagrant-DNS starts a DNS server on a non-privileged port (5300 by default) and makes sure that the host operating system knows this server. If possible, it only does so for a specific, free, TLD (.test). As a fallback, it also supports pass-through, if you really want to play with ICANN domains. 20 | 21 | The DNS server runs on all platforms. 22 | 23 | Design Goals 24 | ============ 25 | 26 | Vagrant-DNS should not require superuser access for anything except reconfiguring the hosts DNS system. This is why there is a seperate "install" step that registers the Vagrant-DNS server to your system (for the configured TLDs). (Re)starts of the server (happening at startup of the machine) need to happen without superuser access. 27 | 28 | macOS 29 | ===== 30 | 31 | As a reference, here is how this works on macOS: 32 | 33 | * `install` places a symlink in `/etc/resolver/{tld}` that links to a corresponding file in `.vagrant.d` with content similar to this one: 34 | 35 | ``` 36 | nameserver 127.0.0.1 37 | port 5300 38 | ``` 39 | 40 | From then on, OS X will resolve all requests to that top level domain using the given resolver. 41 | 42 | Linux 43 | ===== 44 | 45 | With `systemd-resolved`, we finally got a mechanism that resembles the macOS approach, with some subtile differences. 46 | 47 | * `install` creates a single file `vagrant-dns.conf`, even when multiple top level domains are configured: 48 | 49 | ``` 50 | [Resolve] 51 | DNS=127.0.0.1:5300 52 | Domains=~test ~othertld 53 | ``` 54 | 55 | This file is then copied into `/etc/systemd/resolved.conf.d/`. The macOS strategy of adding symlinks from config files to a file within `/home//` doesn't work since the user that runs systemd-resolved doesn't have permission to read there. 56 | 57 | This makes zombie configurations likely. For example when `vagrant-dns` is not correctly uninstalled using its `uninstall` command, its `resolved` configuration will point to a non-existing server, potentially causing troubles. 58 | 59 | (Open)BSD 60 | ========= 61 | 62 | Here, the question is simple: Is editing resolv.conf an option? Any takers? 63 | 64 | Windows 65 | ======= 66 | 67 | Quite frankly, I have not enough understanding on how to implement this properly on Windows. 68 | 69 | General Notes 70 | ============= 71 | 72 | mDNS is not an option. It doesn't support subdomains and most implementations do not support multiple servers per machine. Thats sad, as all platforms come with good solutions for it. 73 | -------------------------------------------------------------------------------- /test/acceptance/dns/static_public_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'provider/dns_static_public' do |provider, options| 2 | 3 | if options[:box] && !File.file?(options[:box]) 4 | raise ArgumentError, 5 | "A box file #{options[:box]} must be downloaded for provider: #{provider}. Try: rake acceptance:setup" 6 | end 7 | 8 | include_context 'acceptance' 9 | let(:tmp_path) { environment.homedir } 10 | 11 | let(:box_ip) { '10.10.10.102' } 12 | let(:tld) { 'spec' } 13 | let(:name) { 'public.testbox.spec' } 14 | 15 | before do 16 | ENV['VAGRANT_DEFAULT_PROVIDER'] = provider 17 | environment.skeleton('dns_static_public') 18 | end 19 | 20 | describe 'installation' do 21 | it 'creates and removes resolver link' do 22 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 23 | assert_execute('sudo', 'ls', "/etc/resolver/#{tld}") 24 | 25 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 26 | assert_execute('sudo', 'test', '!', '-f', "/etc/resolver/#{tld}") 27 | end 28 | 29 | it 'creates config file' do 30 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 31 | 32 | # writes config file 33 | result = execute('sudo', 'cat', "#{tmp_path}/tmp/dns/config") 34 | expect(result).to exit_with(0) 35 | expect(result.stdout).to include(%Q|"^.*#{name}$": #{box_ip}|) 36 | 37 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 38 | end 39 | end 40 | 41 | describe 'running' do 42 | before do 43 | assert_execute('vagrant', 'box', 'add', 'box', options[:box]) 44 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 45 | # skipping "up" here speeds up our specs 46 | # assert_execute('vagrant', 'up', "--provider=#{provider}") 47 | assert_execute('vagrant', 'dns', '--start') 48 | end 49 | 50 | after do 51 | # Ensure any VMs that survived tests are cleaned up. 52 | execute('vagrant', 'destroy', '--force', log: false) 53 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 54 | end 55 | 56 | it 'auto-starts the DNS daemon' do 57 | assert_execute('pgrep', '-lf', 'vagrant-dns') 58 | end 59 | 60 | it 'registered as a resolver' do 61 | # the output of `scutil` changes it's format over MacOS versions a bit 62 | expected_output = Regexp.new(<<-TXT.gsub(/^\s*/, ''), Regexp::MULTILINE) 63 | \\s*domain\\s*: #{tld} 64 | \\s*nameserver\\[0\\]\\s*: 127.0.0.1 65 | \\s*port\\s*: 5333 66 | \\s*flags\\s*: Request A records, Request AAAA records 67 | \\s*reach\\s*:(?=.*\\bReachable\\b)(?=.*\\bLocal Address\\b).* 68 | TXT 69 | 70 | result = assert_execute('scutil', '--dns') 71 | expect(result.stdout).to match(expected_output) 72 | end 73 | 74 | it 'responds to host-names' do 75 | result = assert_execute('dscacheutil', '-q', 'host', '-a', 'name', "#{name}") 76 | expect(result.stdout).to include("ip_address: #{box_ip}") 77 | 78 | result = assert_execute('dscacheutil', '-q', 'host', '-a', 'name', "www.#{name}") 79 | expect(result.stdout).to include("ip_address: #{box_ip}") 80 | 81 | result = execute('dscacheutil', '-q', 'host', '-a', 'name', "notthere.#{tld}") 82 | expect(result.stdout).to_not include("ip_address: #{box_ip}") 83 | end 84 | 85 | it 'vagrant box starts up and is usable' do 86 | assert_execute('vagrant', 'up', "--provider=#{provider}") 87 | result = assert_execute('vagrant', 'ssh', '-c', 'whoami') 88 | expect(result.stdout).to include("vagrant") 89 | end 90 | end 91 | 92 | describe 'un-configure' do 93 | before do 94 | assert_execute('vagrant', 'box', 'add', 'box', options[:box]) 95 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 96 | assert_execute('vagrant', 'dns', '--start') 97 | end 98 | 99 | after do 100 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 101 | end 102 | 103 | it "removes the host name config on destroy" do 104 | result = assert_execute('vagrant', 'dns', '--list') 105 | expect(result.stdout).to include(%Q|/^.*#{name}$/ => #{box_ip}|) 106 | 107 | result = assert_execute('dscacheutil', '-q', 'host', '-a', 'name', "#{name}") 108 | expect(result.stdout).to include("ip_address: #{box_ip}") 109 | 110 | # don't assert, since it was not created in the first place, but will 111 | # still trigger the "machine_action_destroy" event 112 | execute('vagrant', 'destroy', '--force') 113 | 114 | result = assert_execute('vagrant', 'dns', '--list') 115 | expect(result.stdout).to_not include(name) 116 | expect(result.stdout).to_not include(box_ip) 117 | expect(result.stdout).to include("Pattern configuration missing or empty.") 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/vagrant-dns/command.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require 'daemons' 3 | require 'vagrant' 4 | 5 | module VagrantDNS 6 | 7 | class Command < Vagrant.plugin(2, :command) 8 | 9 | # Runs the vbguest installer on the VMs that are represented 10 | # by this environment. 11 | def execute 12 | options = {} 13 | opts = OptionParser.new do |opts| 14 | opts.banner = "Usage: vagrant dns [vm-name] [-i|--install] [-u|--uninstall] [--with-sudo] [--pruge] [-s|--start] [-S|--stop] [-r|--restart] [--status] [-l|--list] [-o|--ontop]" 15 | opts.separator "" 16 | 17 | opts.on("--install", "-i", "Install DNS config for machine domain") do 18 | options[:install] = true 19 | options[:build_config] = true 20 | options[:manage_installation] = true 21 | end 22 | 23 | opts.on("--uninstall", "-u", "Uninstall DNS config for machine domain") do 24 | options[:uninstall] = true 25 | options[:build_config] = false 26 | options[:manage_installation] = true 27 | options[:stop] = true 28 | end 29 | 30 | opts.on("--purge", "Uninstall DNS config and remove DNS configurations of all machines.") do 31 | options[:purge] = true 32 | options[:build_config] = false 33 | options[:manage_installation] = true 34 | options[:stop] = true 35 | end 36 | 37 | opts.on("--start", "-s", "Start the DNS service") do 38 | options[:start] = true 39 | options[:build_config] = true 40 | end 41 | 42 | opts.on("--stop", "-S", "Stop the DNS service") do 43 | options[:stop] = true 44 | options[:build_config] = false 45 | end 46 | 47 | opts.on("--restart", "-r", "Restart the DNS service") do 48 | options[:restart] = true 49 | options[:build_config] = true 50 | end 51 | 52 | opts.on("--status", "Show DNS service running status and PID.") do 53 | options[:status] = true 54 | options[:build_config] = false 55 | end 56 | 57 | opts.on("--list", "-l", "Show the current DNS service config. This works in conjunction with --start --stop --restart --status.") do 58 | options[:show_config] = true 59 | options[:build_config] = false 60 | end 61 | 62 | opts.on("--list-tlds", "-L", "Show the current TopLevelDomains registered. This works in conjunction with --start --stop --restart --status.") do 63 | options[:show_tld_config] = true 64 | options[:build_config] = false 65 | end 66 | 67 | opts.on("--with-sudo", "In conjunction with `--install`, `--uninstall`, `--purge`: Run using `sudo` instead of `osascript`. Useful for automated scripts running as sudoer.") do 68 | options[:installer_opts] = { exec_style: :sudo } 69 | end 70 | 71 | opts.on("--ontop", "-o", "Start the DNS service on top. Debugging only, this blocks Vagrant!") do 72 | options[:ontop] = true 73 | end 74 | end 75 | 76 | argv = parse_options(opts) 77 | return if !argv 78 | 79 | vms = argv unless argv.empty? 80 | 81 | build_config(vms, options) if options[:build_config] 82 | manage_service(vms, options) 83 | manage_installation(vms, options) if options[:manage_installation] 84 | show_config(vms, options) if options[:show_config] || options[:show_tld_config] 85 | end 86 | 87 | protected 88 | 89 | def manage_installation(vms, options) 90 | installer_class = VagrantDNS::Installers.resolve 91 | 92 | installer_options = options.fetch(:installer_opts, {}) 93 | installer = installer_class.new(tmp_path, installer_options) 94 | 95 | if options[:install] 96 | installer.install! 97 | elsif options[:uninstall] 98 | installer.uninstall! 99 | elsif options[:purge] 100 | installer.purge! 101 | end 102 | end 103 | 104 | def manage_service(vms, options) 105 | service = VagrantDNS::Service.new(tmp_path) 106 | 107 | if options[:start] 108 | service.start! :ontop => options[:ontop] 109 | elsif options[:stop] 110 | service.stop! 111 | elsif options[:restart] 112 | service.restart! :ontop => options[:ontop] 113 | elsif options[:status] 114 | service.status! 115 | end 116 | end 117 | 118 | def show_config(vms, options) 119 | service = VagrantDNS::Service.new(tmp_path) 120 | service.show_tld_config if options[:show_tld_config] 121 | service.show_config if options[:show_config] 122 | end 123 | 124 | def build_config(vms, options) 125 | with_target_vms(vms) { |vm| VagrantDNS::Configurator.new(vm, tmp_path).up! } 126 | end 127 | 128 | def tmp_path 129 | File.join(@env.tmp_path, "dns") 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /test/acceptance/dns/static_private_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'provider/dns_static_private' do |provider, options| 2 | 3 | if options[:box] && !File.file?(options[:box]) 4 | raise ArgumentError, 5 | "A box file #{options[:box]} must be downloaded for provider: #{provider}. Try: rake acceptance:setup" 6 | end 7 | 8 | include_context 'acceptance' 9 | let(:tmp_path) { environment.homedir } 10 | 11 | let(:box_ip) do 12 | case provider 13 | when "virtualbox" then '10.10.10.101' 14 | when "vmware_desktop" then '192.168.134.111' 15 | end 16 | end 17 | let(:tld) { 'spec' } 18 | let(:name) { 'private.testbox.spec' } 19 | 20 | before do 21 | ENV['VAGRANT_DEFAULT_PROVIDER'] = provider 22 | environment.skeleton('dns_static_private') 23 | end 24 | 25 | describe 'installation' do 26 | it 'creates and removes resolver link' do 27 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 28 | assert_execute('sudo', 'test', '-f', "/etc/resolver/#{tld}") 29 | 30 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 31 | assert_execute('sudo', 'test', '!', '-f', "/etc/resolver/#{tld}") 32 | end 33 | 34 | it 'creates config file' do 35 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 36 | 37 | # writes config file 38 | result = execute('sudo', 'cat', "#{tmp_path}/tmp/dns/config") 39 | expect(result).to exit_with(0) 40 | expect(result.stdout).to include(%Q|"^.*#{name}$": #{box_ip}|) 41 | 42 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 43 | end 44 | end 45 | 46 | describe 'running' do 47 | before do 48 | assert_execute('vagrant', 'box', 'add', 'box', options[:box]) 49 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 50 | # skipping "up" here speeds up our specs 51 | # assert_execute('vagrant', 'up', "--provider=#{provider}") 52 | assert_execute('vagrant', 'dns', '--start') 53 | end 54 | 55 | after do 56 | # Ensure any VMs that survived tests are cleaned up. 57 | execute('vagrant', 'destroy', '--force', log: false) 58 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 59 | end 60 | 61 | it 'auto-starts the DNS daemon' do 62 | assert_execute('pgrep', '-lf', 'vagrant-dns') 63 | end 64 | 65 | it 'registered as a resolver' do 66 | # the output of `scutil` changes it's format over MacOS versions a bit 67 | expected_output = Regexp.new(<<-TXT.gsub(/^\s*/, ''), Regexp::MULTILINE) 68 | \\s*domain\\s*: #{tld} 69 | \\s*nameserver\\[0\\]\\s*: 127.0.0.1 70 | \\s*port\\s*: 5333 71 | \\s*flags\\s*: Request A records, Request AAAA records 72 | \\s*reach\\s*:(?=.*\\bReachable\\b)(?=.*\\bLocal Address\\b).* 73 | TXT 74 | 75 | result = assert_execute('scutil', '--dns') 76 | expect(result.stdout).to match(expected_output) 77 | end 78 | 79 | it 'responds to host-names' do 80 | result = assert_execute('dscacheutil', '-q', 'host', '-a', 'name', "#{name}") 81 | expect(result.stdout).to include("ip_address: #{box_ip}") 82 | 83 | result = assert_execute('dscacheutil', '-q', 'host', '-a', 'name', "www.#{name}") 84 | expect(result.stdout).to include("ip_address: #{box_ip}") 85 | 86 | result = execute('dscacheutil', '-q', 'host', '-a', 'name', "notthere.#{tld}") 87 | expect(result.stdout).to_not include("ip_address: #{box_ip}") 88 | end 89 | 90 | it 'vagrant box starts up and is usable' do 91 | assert_execute('vagrant', 'up', "--provider=#{provider}") 92 | result = assert_execute('vagrant', 'ssh', '-c', 'whoami') 93 | expect(result.stdout).to include("vagrant") 94 | end 95 | end 96 | 97 | describe 'un-configure' do 98 | before do 99 | assert_execute('vagrant', 'box', 'add', 'box', options[:box]) 100 | assert_execute('vagrant', 'dns', '--install', '--with-sudo') 101 | assert_execute('vagrant', 'dns', '--start') 102 | end 103 | 104 | after do 105 | assert_execute('vagrant', 'dns', '--uninstall', '--with-sudo') 106 | end 107 | 108 | it "removes the host name config on destroy" do 109 | result = assert_execute('vagrant', 'dns', '--list') 110 | expect(result.stdout).to include(%Q|/^.*#{name}$/ => #{box_ip}|) 111 | 112 | result = assert_execute('dscacheutil', '-q', 'host', '-a', 'name', "#{name}") 113 | expect(result.stdout).to include("ip_address: #{box_ip}") 114 | 115 | # don't assert, since it was not created in the first place, but will 116 | # still trigger the "machine_action_destroy" event 117 | execute('vagrant', 'destroy', '--force') 118 | 119 | result = assert_execute('vagrant', 'dns', '--list') 120 | expect(result.stdout).to_not include(name) 121 | expect(result.stdout).to_not include(box_ip) 122 | expect(result.stdout).to include("Pattern configuration missing or empty.") 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.4.2 (2025-06-27) 2 | 3 | * Pin async-dns to < 1.4 to avoid breaking changes in its API. [GH-85] 4 | 5 | ## 2.4.1 (2023-04-16) 6 | 7 | * Fix plugin hooking for multi-machine setups. [GH-78] 8 | 9 | ## 2.4.0 (2023-04-11) 10 | 11 | * DHCP support. [GH-51] 12 | * Non-A-queries (like IPv6 AAAA- or TXT records) matching a configured pattern will be answered with a `NOTIMP` error. [GH-76] 13 | * Upstream / passthrough DNS servers are now configurable, defaulting to the system config (existing behavior). [GH-76] 14 | * Passthrough behavior is now configurable (`true`/`false`/`:unknown`). [GH-77] 15 | 16 | ## 2.3.0 (2023-01-14) 17 | 18 | * 🎉 Linux Support. Many thanks @mattiasb [GH-75] 19 | * Managed TLDs are now stored in separate config file, which can be inspected via `vagrant dns --list-ltds`. 20 | 21 | ## 2.2.3 22 | 23 | ### Fixes: 24 | 25 | * Workaround for a Vargant-on-Ventura issue, crashing the DNS process. [GH-72] 26 | 27 | ## 2.2.2 28 | 29 | ### Fixes: 30 | 31 | * Installation issue due to updated sub-dependency. [GH-69] 32 | 33 | ## 2.2.1 34 | 35 | ### Fixes: 36 | 37 | * Prevent action hooks (restarting dns server) from running multiple times [GH-67] 38 | 39 | ## 2.2.0 40 | 41 | ### New Features: 42 | 43 | * Add global config for time-to-live via `VagrantDNS::Config.ttl` 44 | 45 | ### Fixes: 46 | 47 | * Fixes acceptance tests for macOS High Sierra (10.13) to cope with `scutil`s new output format 48 | * Adds the log-time missing `license` gem config 49 | 50 | ### Breaking Changes: 51 | 52 | * Removes the global and vm config `ipv4only`. It was never used. 53 | 54 | ### Changes: 55 | 56 | * Resources will now respond with a low TTL (time-to-live) of 5 minutes (300 seconds) instead of the old 24 hours by default. Use `VagrantDNS::Config.ttl = 86400` to reset to the old behavior. 57 | * Internal changes on how the dns pattern config is read and written. (Now using `YAML::Store`) 58 | * Acceptance tests run against vagrant v2.1.4 59 | * Acceptance tests run against Ubuntu 18.04 60 | 61 | ### New Features: 62 | 63 | * Adds a check for the VMs configured TLDs to not be included in the [Public Suffix List](https://publicsuffix.org/) 64 | 65 | ## 2.1.0 66 | 67 | ### Breaking, internal changes: 68 | 69 | * The version moved from `Vagrant::DNS::VERSION` to `VagrantDNS::VERSION`, removing an accidental highjack of the `Vagrant` namespace. 70 | * The `VagrantDNS::RestartMiddleware` got split up into `VagrantDNS::Middlewares::ConfigUp`, `VagrantDNS::Middlewares::ConfigDown` and `VagrantDNS::Middlewares::Restart` 71 | 72 | ### Fixes: 73 | 74 | * Fixes cli short argument `-S` for `--stop` (collided with `--start`'s `-s`) 75 | 76 | ### New Feautres: 77 | 78 | * Adds new cli command `--status` to display process running status and PID 79 | * Adds new cli command `--list` to display current (persisted) config 80 | * Adds auto-cleanup: Removes registered dns pattern from config when destroying the box. It will, however, be re-created when some box in the project gets re/started or one of vagrant dns `--install`, `--start` or `--restart` is executed. 81 | 82 | ### Changes: 83 | 84 | * The cli command `--stop` no longer re-builds configuration. 85 | 86 | ## 2.0.0 87 | 88 | * Upgrades RubyDNS to `2.0` release 89 | 90 | ## 2.0.0.rc1 91 | 92 | * Upgrades RubyDNS to `2.0.0.pre.rc2`, which removes it's dependency on `celluloid`/`celluloid-dns` 🎉 93 | * Requires Vagrant >= 1.9.6 which ships with ruby 2.3.4 (RubyDNS requires ruby >= 2.2.6) 94 | * Development note: Upgraded to vagrant-share HEAD (d558861f) 95 | 96 | ## 1.1.0 97 | 98 | * Fixes handling of networks without static IP, such as DHCP. [GH-37], [GH-39], [GH-50] 99 | * Add support for boxes with `public_network` and static IP. 100 | * Breaking: No longer falls back to `127.0.0.1` when no IP could be found. 101 | * Log messages will now be tagged with the related box (vm) and `[vagrant-dns]` 102 | * Development targets Vagrant 1.9.3 103 | 104 | ## 1.0.0 105 | 106 | * 🎉Release as 1.0 [GH-34] 107 | * Fixes compatibility to Vagrant 1.7.4 by using RubyDNS ~> 1.0.2 [GH-38] 108 | 109 | ## 0.6.0 110 | 111 | This is a intermediate release towards v1.0.0 112 | 113 | * Using RubyDNS ~> 0.9.0 114 | * New option to un/install system files using `sudo` (restoring 0.4 behavior). Add optional `--with-sudo` flag to `--install`, `--uninstall` or `--purge`. [GH-26] 115 | * Re-add `--ontop` flag. (Start `vagrant-dns` in foreground for debugging) 116 | * Use vagrant >= 1.5 development patterns [GH-31] 117 | * Add acceptance test using vagrant-spec 118 | * Moved sample `Vagrantfile` into `/testdrive` which also contains a small wrapper script `bin/vagrant` to move "vagrant home" into a sandbox. 119 | 120 | ## 0.5.0 121 | 122 | * Use `osascript` to install system files, which require root privileges. [GH-18], [GH-22] 123 | * internal cleanups (@ringods) 124 | 125 | ## v0.4.1 126 | 127 | * Fixes an issue with not configured private networks [GH-21], [GH-19] 128 | 129 | ## v0.4.0 130 | 131 | **This version is not compatible to vagrant < 1.2** 132 | 133 | * Supports vagrant 1.2.x [GH-17] 134 | * Update RubyDNS ~> 0.6.0 135 | * Fixes an issue where user-space files got created as root [GH-12] 136 | * Adds command line option `--purge` which deletes the created config file (not only the system symlink to it) [GH-3] 137 | 138 | ## v0.3.0 139 | 140 | * Using RubyDNS ~> 0.5.3 141 | 142 | ## v0.2.5 143 | 144 | * Using RubyDNS 0.4.x 145 | 146 | ## v0.2.4 147 | 148 | * Enable passthrough in DNS service. Helpful if you insist to hook a standard tld like .io. 149 | 150 | ## v0.2.3 151 | 152 | * Remove unneeded iteraton in installers/mac.rb. (thanks to JonathanTron for reporting) 153 | * Fix crash when no tld option was given. 154 | 155 | ## v0.2.2 156 | 157 | * Default to 127.0.0.1 when no network option is given 158 | * Warn if no default pattern is configured because TLD is given, but no host_name 159 | 160 | ## v0.2.1 161 | 162 | * Fix a crash when /etc/resolver is not present 163 | 164 | ## v0.2.0 165 | 166 | * Deactivate ipv6 support (was broken anyways) 167 | * Add multiple patterns support 168 | * Introduces the `tlds` config key, for managing multiple tlds. `tld` is still supported 169 | * --ontop option to run the dns server on top 170 | * removed the logger config in favor of a default logger 171 | * Uses one central server now 172 | * The DNS server is now reconfigured on every run 173 | 174 | ## v0.1.0 175 | 176 | * Initial Release 177 | * Support for A and AAAA configuration 178 | -------------------------------------------------------------------------------- /lib/vagrant-dns/configurator.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | module VagrantDNS 4 | class Configurator 5 | attr_accessor :vm, :tmp_path 6 | 7 | def initialize(vm, tmp_path) 8 | @vm = vm 9 | @tmp_path = tmp_path 10 | end 11 | 12 | def up! 13 | return unless validate_tlds 14 | regenerate_resolvers! 15 | ensure_deamon_env! 16 | register_patterns! 17 | end 18 | 19 | def down! 20 | unregister_patterns! 21 | end 22 | 23 | private def validate_tlds 24 | valid, err = VagrantDNS::Config.validate_tlds(vm) 25 | if !valid 26 | vm.ui.error(err) 27 | elsif err 28 | vm.ui.warn(err) 29 | end 30 | valid 31 | end 32 | 33 | private def regenerate_resolvers! 34 | resolver_folder = self.resolver_folder 35 | _proto, ip, port = VagrantDNS::Config.listen.first 36 | tlds = register_tlds! 37 | 38 | resolver_files(ip, port, tlds) do |filename, contents| 39 | File.write(File.join(resolver_folder, filename), contents) 40 | end 41 | end 42 | 43 | private def register_tlds! 44 | add_tlds = dns_options(vm)[:tlds] 45 | VagrantDNS::TldRegistry 46 | .new(tmp_path) 47 | .transaction do |store| 48 | store["tlds"] ||= [] 49 | store["tlds"] |= add_tlds 50 | store["tlds"] 51 | end 52 | end 53 | 54 | private def register_patterns! 55 | opts = dns_options(vm) 56 | 57 | patterns = opts[:patterns] || default_patterns(opts) 58 | if patterns.empty? 59 | vm.ui.warn "[vagrant-dns] TLD but no host_name given. No patterns will be configured." 60 | return 61 | end 62 | 63 | ip = vm_ip(opts) 64 | unless ip 65 | vm.ui.detail "[vagrant-dns] No patterns will be configured." 66 | return 67 | end 68 | 69 | registry = Registry.new(tmp_path) 70 | registry.transaction do 71 | patterns.each { |pattern| registry[pattern] = ip } 72 | end 73 | end 74 | 75 | private def unregister_patterns! 76 | opts = dns_options(vm) 77 | 78 | patterns = opts[:patterns] || default_patterns(opts) 79 | if patterns.empty? 80 | vm.ui.warn "[vagrant-dns] TLD but no host_name given. No patterns will be removed." 81 | return 82 | end 83 | 84 | registry = Registry.open(tmp_path) 85 | return unless registry 86 | 87 | registry.transaction do 88 | unless registry.any? 89 | vm.ui.warn "[vagrant-dns] Configuration missing or empty. No patterns will be removed." 90 | registry.abort 91 | end 92 | 93 | patterns.each do |pattern| 94 | if (ip = registry.delete(pattern)) 95 | vm.ui.info "[vagrant-dns] Removing pattern: #{pattern} for ip: #{ip}" 96 | else 97 | vm.ui.info "[vagrant-dns] Pattern: #{pattern} was not in config." 98 | end 99 | end 100 | end 101 | end 102 | 103 | private def dns_options(vm) 104 | return @dns_options if @dns_options 105 | 106 | @dns_options = vm.config.dns.to_hash 107 | @dns_options[:host_name] = vm.config.vm.hostname 108 | @dns_options[:networks] = vm.config.vm.networks 109 | @dns_options 110 | end 111 | 112 | private def default_patterns(opts) 113 | if opts[:host_name] 114 | opts[:tlds].map { |tld| /^.*#{opts[:host_name]}.#{tld}$/ } 115 | else 116 | [] 117 | end 118 | end 119 | 120 | private def vm_ip(opts) 121 | user_ip = opts[:ip] 122 | 123 | if !user_ip && dynamic_ip_network?(opts) || [:dynamic, :dhcp].include?(user_ip) 124 | user_ip = DYNAMIC_VM_IP 125 | end 126 | 127 | ip = 128 | case user_ip 129 | when Proc 130 | if vm.communicate.ready? 131 | user_ip.call(vm, opts.dup.freeze) 132 | else 133 | vm.ui.info "[vagrant-dns] Postponing running user provided IP script until box has started." 134 | return 135 | end 136 | when Symbol 137 | _ip = static_vm_ip(user_ip, opts) 138 | 139 | unless _ip 140 | vm.ui.warn "[vagrant-dns] Could not find any static network IP in network type `#{user_ip}'." 141 | return 142 | end 143 | 144 | _ip 145 | else 146 | _ip = static_vm_ip(:private_network, opts) 147 | _ip ||= static_vm_ip(:public_network, opts) 148 | 149 | unless _ip 150 | vm.ui.warn "[vagrant-dns] Could not find any static network IP." 151 | return 152 | end 153 | 154 | _ip 155 | end 156 | 157 | # we where unable to find an IP, and there's no user-supplied callback 158 | # falling back to dynamic/dhcp style detection 159 | if !ip && !user_ip 160 | vm.ui.info "[vagrant-dns] Falling back to dynamic IP detection." 161 | ip = DYNAMIC_VM_IP.call(vm, opts.dup.freeze) 162 | end 163 | 164 | if !ip || ip.empty? 165 | vm.ui.warn "[vagrant-dns] Failed to identify IP." 166 | return 167 | end 168 | 169 | ip 170 | end 171 | 172 | private def dynamic_ip_network?(opts) 173 | opts[:networks].none? { |(_nw_type, nw_config)| nw_config[:ip] } 174 | end 175 | 176 | # tries to find an IP in the configured +type+ networks 177 | private def static_vm_ip(type, opts) 178 | network = opts[:networks].find { |(nw_type, nw_config)| nw_config[:ip] && nw_type == type } 179 | 180 | network.last[:ip] if network 181 | end 182 | 183 | DYNAMIC_VM_IP = proc { |vm| 184 | vm.guest.capability(:read_ip_address).tap { |ip| 185 | if ip 186 | vm.ui.info "[vagrant-dns] Identified DHCP IP as '#{ip}'." 187 | else 188 | vm.ui.warn "[vagrant-dns] Could not identify DHCP IP." 189 | end 190 | } 191 | } 192 | private_constant :DYNAMIC_VM_IP 193 | 194 | private def resolver_files(ip, port, tlds, &block) 195 | installer_class = VagrantDNS::Installers.resolve 196 | installer_class.resolver_files(ip, port, tlds, &block) 197 | end 198 | 199 | private def resolver_folder 200 | File.join(tmp_path, "resolver").tap { |dir| FileUtils.mkdir_p(dir) } 201 | end 202 | 203 | private def ensure_deamon_env! 204 | FileUtils.mkdir_p(File.join(tmp_path, "daemon")) 205 | end 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vagrant-dns 2 | 3 | `vagrant-dns` allows you to configure a dns-server managing a development subdomain. It works much like pow, but manages Vagrant machines. 4 | 5 | ## Installation 6 | 7 | $ vagrant plugin install vagrant-dns 8 | 9 | **Attention: As of v2.0.0, vagrant-dns requires vagrant >= 1.9.6** (because it ships with a more modern version of ruby) 10 | If you get an error like `rubydns requires Ruby version >= 2.2.6.` while installing, you probably need to upgrade vagrant. 11 | Alternatively, you can install an older version of vagrant-dns like this: `vagrant plugin install --plugin-version="<2" vagrant-dns` 12 | 13 | ## Usage 14 | 15 | In addition to your networking config, configure a top-level domain and a `hostname` for your machine. Optionally, configure a set of free matching patterns. Global configuration options can be given through the `VagrantDNS::Config` object: 16 | 17 | ```ruby 18 | Vagrant.configure("2") do |config| 19 | # ... 20 | 21 | config.dns.tld = "test" 22 | 23 | config.vm.hostname = "machine" 24 | 25 | config.dns.patterns = [/^(\w+\.)*mysite\.test$/, /^(\w+\.)*myothersite\.test$/] 26 | 27 | config.vm.network :private_network, ip: "33.33.33.60" 28 | end 29 | 30 | # optional 31 | VagrantDNS::Config.logger = Logger.new("dns.log") 32 | ``` 33 | 34 | Then, register the DNS server as a resolver: 35 | 36 | ```bash 37 | $ vagrant dns --install 38 | ``` 39 | 40 | This command will write out a configuration file that tells the operating system to resolve the `.test` TLD via the `vagrant-dns` DNS server. On OS X, this config file is located at `/etc/resolver/test` while on Linux it's at `/etc/systemd/resolved.conf.d/vagrant-dns.conf`. 41 | 42 | You will have to rerun --install every time a tld is added. 43 | 44 | You can delete this file by running: 45 | 46 | ```bash 47 | $ vagrant dns --uninstall 48 | ``` 49 | 50 | To also delete the created config file for this TLD (`~/.vagrant.d/tmp/dns/resolver/test` or `~/.vagrant.d/tmp/dns/resolver/vagrant-dns.conf` in our example) run: 51 | 52 | 53 | ```bash 54 | $ vagrant dns --purge 55 | ``` 56 | 57 | Then, run the DNS server: 58 | 59 | ```bash 60 | $ vagrant dns --start 61 | ``` 62 | 63 | And test it: 64 | 65 | **OS X:** 66 | 67 | ```bash 68 | $ scutil --dns 69 | [ … ] 70 | resolver #8 71 | domain : test 72 | nameserver[0] : 127.0.0.1 73 | port : 5300 74 | [ … ] 75 | 76 | $ dscacheutil -q host -a name test.machine.test 77 | name: test.machine.test 78 | ip_address: 33.33.33.10 79 | ``` 80 | 81 | **Linux**: 82 | 83 | ```bash 84 | $ resolvectl status 85 | Global 86 | Protocols: LLMNR=resolve -mDNS -DNSOverTLS DNSSEC=no/unsupported 87 | resolv.conf mode: stub 88 | DNS Domain: ~test 89 | [ … ] 90 | 91 | $ resolvectl query test.machine.test 92 | test.machine.test: 33.33.33.10 93 | 94 | -- Information acquired via protocol DNS in 10.1420s. 95 | -- Data is authenticated: no; Data was acquired via local or encrypted transport: no 96 | ``` 97 | 98 | **Note:** Mac OS X is quite different from Linux regarding DNS resolution. As a result, do not use 99 | `dig` or `nslookup`, but `dscacheutil` instead. Read [this article](http://apple.stackexchange.com/a/70583) 100 | for more information. 101 | 102 | 103 | You can now reach the server under the given domain. 104 | 105 | ```bash 106 | $ vagrant dns --stop 107 | ``` 108 | 109 | The DNS server will start automatically once the first VM is started. 110 | 111 | You can list the currently configured dns patterns using: 112 | 113 | ```bash 114 | $ vagrant dns --list 115 | ``` 116 | 117 | (Keep in mind, that it's not guaranteed that the running server uses exactly this configuration - for example, when manually editing it.) 118 | The output looks somewhat like this: 119 | 120 | ``` 121 | /^.*mysite.test$/ => 33.33.33.60 122 | /^.*myothersite.test$/ => 33.33.33.60 123 | ``` 124 | 125 | Where the first part of each line is a [regular expression](https://ruby-doc.org/core-2.3.0/Regexp.html) and the second part is the mapped IPv4. (` => ` is just a separator) 126 | 127 | ## Multi VM config 128 | 129 | We can use [multivm](https://www.vagrantup.com/docs/multi-machine/) configuration and have dns names for host. 130 | 131 | * Use below given vagrant config 132 | ```ruby 133 | BOX_IMAGE = "ubuntu/xenial64" 134 | WORKER_COUNT = 2 135 | Vagrant.configure("2") do |config| 136 | (1..WORKER_COUNT).each do |i| 137 | config.vm.define "worker#{i}" do |subconfig| 138 | subconfig.dns.tld = "test" 139 | subconfig.vm.hostname = "vagrant" 140 | subconfig.dns.patterns = "worker#{i}.mysite.test" 141 | subconfig.vm.box = BOX_IMAGE 142 | subconfig.vm.network "private_network", ip: "10.240.0.#{i+15}" 143 | end 144 | end 145 | end 146 | ``` 147 | * `vagrant up` 148 | * Execute : `vagrant dns --install` 149 | * Test via: `ping worker2.mysite.box` or `worker1.mysite.box` 150 | 151 | ## VM options 152 | 153 | * `vm.dns.tld`: Set the tld for the given virtual machine. No default. 154 | * `vm.dns.tlds`: Set multiple TLDs. Default: `[tld]` 155 | * `vm.dns.patterns`: A list of domain patterns to match. Defaults to `[/^.*{host_name}.{tld}$/]` 156 | * `vm.dns.ip`: Optional, overwrite of the default static IP detection. Valid values are: 157 | - `Proc`: A Proc which return value will get used as IP. Eg: `proc { |vm, opts| }` (See DHCP section for full sample) 158 | - `Symbol`: Forces the use of the named static network: 159 | + `:private_network`: Use static ip of a configured private network (`config.vm.network "private_network", ip: "192.168.50.4"`) 160 | + `:public_network`: Use static ip of a configured public network (`config.vm.network "public_network", ip: "192.168.0.17"`) 161 | + `:dynamic`, `:dhcp`: Force reading the guest IP using vagrants build-in `read_ip_address` capability. 162 | - Default: 163 | If there is no network with a statically defined IP, and no `ip` override is given, use the build-in `read_ip_address` capability. 164 | Else, check `:private_network`, if none found check `:public_network` 165 | 166 | ## Global Options 167 | 168 | * `VagrantDNS::Config.listen`: an Array of Arrays describing interfaces to bind to. Defaults to `[[:udp, "127.0.0.1", 5300]]`. 169 | * `VagrantDNS::Config.ttl`: The time-to-live in seconds for all resources. Defaults to `300` (5 minutes). 170 | * `VagrantDNS::Config.auto_run`: (re)start and reconfigure the server every time a machine is started. On by default. 171 | * `VagrantDNS::Config.check_public_suffix`: Check if you are going to configure a [Public Suffix](https://publicsuffix.org/) (like a Top Level Domain) in a VMs `tld(s)` config, which could mess up your local dev machines DNS config. Possible configuration values are: 172 | - `false`: Disables the feature. 173 | - `"warn"`: Check and print a warning. (Still creates a resolver config and potentially messes up your DNS) **At the moment, this is the default** because lots of projects used to use `"dev"` as a TLD, but this got registered by google and is now a public suffix. 174 | - `"error"`: Check and prevent the box from starting. (Does not create the resolver config, it will also prevent the box from starting.) 175 | * `VagrantDNS::Config.passthrough`: Configure how to deal with non-matching DNS queries. Will be set to `false` if no `passthrough_resolver` could be detected, or none is set. 176 | - `true`: (default) Every non-matching query is passed through to the upstream DNS server (see `passthrough_resolver`) 177 | - `false`: Disable passthrough. Every non-A-query to a matching pattern fails with `NotImp`. Every other query fails with `NXDomain`. 178 | - `:unknown`: Only forward queries for which there's no matching pattern. Every non-A-query to a matching pattern fails with `NotImp`. Every other query will be passed through. 179 | * `VagrantDNS::Config.passthrough_resolver`: By default, DNS queries not matching any patterns will be passed to an upstream DNS server. 180 | - `:system`: (Default) pass through to the system configured default DNS server 181 | - `[[:udp, "1.1.1.1", 53], [:tcp, "1.1.1.1", 53]]`: an Array of Arrays describing the DNS server(s) to use. (Protocol, IP, Port) 182 | 183 | ## Using custom domains from inside the VM (VirtualBox only) 184 | 185 | If you need to be able to resolve custom domains managed by this plugin from inside your virtual machine (and you're using VirtualBox), add the following 186 | setting to your `Vagrantfile`: 187 | 188 | ```ruby 189 | Vagrant.configure(2) do |config| 190 | # ... 191 | config.vm.provider "virtualbox" do |vm_config, override| 192 | vm_config.customize [ 193 | "modifyvm", :id, 194 | "--natdnshostresolver1", "on", 195 | # some systems also need this: 196 | # "--natdnshostresolver2", "on" 197 | ] 198 | end 199 | end 200 | ``` 201 | 202 | By default, the Virtualbox NAT engine offers the same DNS servers to the guest that are configured on the host. With the above 203 | setting, however, the NAT engine will act as a DNS proxy 204 | (see [Virtualbox docs](https://www.virtualbox.org/manual/ch09.html#nat-adv-dns)). That way, queries for your custom domains 205 | from inside the guest will also be handled by the DNS server run by the plugin. 206 | 207 | ## DHCP / Dynamic IP 208 | 209 | When configuring your VM with a DHCP network, vagrant-dns tries to identify the guest IP using vagrants build-in `read_ip_address` capability. 210 | 211 | For more fine-grained control use the `ip` config. 212 | Here is an example using the VM's default way of communication and using the `hostname` command on it: 213 | 214 | ```ruby 215 | Vagrant.configure("2") do |config| 216 | # ... 217 | 218 | # - `vm` is the vagrant virtual machine instance and can be used to communicate with it 219 | # - `opts` is the vagrant-dns options hash (everything configured via `config.dns.*`) 220 | config.dns.ip = -> (vm, opts) do 221 | # note: the block handed to `execute` might get called multiple times, hence this closure 222 | ip = nil 223 | vm.communicate.execute("hostname -I | cut -d ' ' -f 1") do |type, data| 224 | ip = data.strip if type == :stdout 225 | end 226 | ip 227 | end 228 | end 229 | ``` 230 | 231 | __NOTES__: In order to obtain the IP in this way, the vagrant box needs to be up and running. You will get a log output (`Postponing running user provided IP script until box has started.`) when you try to run `vagrant dns` on a non-running box. 232 | 233 | ## Issues 234 | 235 | * macOS and systemd-resolved enabled Linux only (please read: [Platform Support](https://github.com/BerlinVagrant/vagrant-dns/blob/master/PLATFORM_SUPPORT.md) before ranting about this). 236 | * `A` records only 237 | * No IPv6 support 238 | * Not automatically visible inside the box (special configuration of your guest system or provider needed) 239 | --------------------------------------------------------------------------------