Browse code

configurable dns search domains

Add a --dns-search parameter and a DnsSearch
configuration field for specifying dns search
domains.

Docker-DCO-1.1-Signed-off-by: Daniel Norberg <daniel.norberg@gmail.com> (github: danielnorberg)

Daniel Norberg authored on 2014/02/08 01:48:14
Showing 13 changed files
... ...
@@ -18,6 +18,7 @@ type Config struct {
18 18
 	Root                        string
19 19
 	AutoRestart                 bool
20 20
 	Dns                         []string
21
+	DnsSearch                   []string
21 22
 	EnableIptables              bool
22 23
 	EnableIpForward             bool
23 24
 	DefaultIp                   net.IP
... ...
@@ -49,6 +50,9 @@ func ConfigFromJob(job *engine.Job) *Config {
49 49
 	if dns := job.GetenvList("Dns"); dns != nil {
50 50
 		config.Dns = dns
51 51
 	}
52
+	if dnsSearch := job.GetenvList("DnsSearch"); dnsSearch != nil {
53
+		config.DnsSearch = dnsSearch
54
+	}
52 55
 	if mtu := job.GetenvInt("Mtu"); mtu != 0 {
53 56
 		config.Mtu = mtu
54 57
 	} else {
... ...
@@ -35,6 +35,7 @@ func main() {
35 35
 		flSocketGroup        = flag.String([]string{"G", "-group"}, "docker", "Group to assign the unix socket specified by -H when running in daemon mode; use '' (the empty string) to disable setting of a group")
36 36
 		flEnableCors         = flag.Bool([]string{"#api-enable-cors", "-api-enable-cors"}, false, "Enable CORS headers in the remote API")
37 37
 		flDns                = opts.NewListOpts(opts.ValidateIp4Address)
38
+		flDnsSearch          = opts.NewListOpts(opts.ValidateDomain)
38 39
 		flEnableIptables     = flag.Bool([]string{"#iptables", "-iptables"}, true, "Enable Docker's addition of iptables rules")
39 40
 		flEnableIpForward    = flag.Bool([]string{"#ip-forward", "-ip-forward"}, true, "Enable net.ipv4.ip_forward")
40 41
 		flDefaultIp          = flag.String([]string{"#ip", "-ip"}, "0.0.0.0", "Default IP address to use when binding container ports")
... ...
@@ -45,6 +46,7 @@ func main() {
45 45
 		flMtu                = flag.Int([]string{"#mtu", "-mtu"}, 0, "Set the containers network MTU; if no value is provided: default to the default route MTU or 1500 if no default route is available")
46 46
 	)
47 47
 	flag.Var(&flDns, []string{"#dns", "-dns"}, "Force docker to use specific DNS servers")
48
+	flag.Var(&flDnsSearch, []string{"-dns-search"}, "Force Docker to use specific DNS search domains")
48 49
 	flag.Var(&flHosts, []string{"H", "-host"}, "tcp://host:port, unix://path/to/socket, fd://* or fd://socketfd to use in daemon mode. Multiple sockets can be specified")
49 50
 
50 51
 	flag.Parse()
... ...
@@ -115,6 +117,7 @@ func main() {
115 115
 			job.Setenv("Root", realRoot)
116 116
 			job.SetenvBool("AutoRestart", *flAutoRestart)
117 117
 			job.SetenvList("Dns", flDns.GetAll())
118
+			job.SetenvList("DnsSearch", flDnsSearch.GetAll())
118 119
 			job.SetenvBool("EnableIptables", *flEnableIptables)
119 120
 			job.SetenvBool("EnableIpForward", *flEnableIpForward)
120 121
 			job.Setenv("BridgeIface", *bridgeName)
... ...
@@ -77,6 +77,7 @@ Commands
77 77
       --bip="": Use this CIDR notation address for the network bridge's IP, not compatible with -b
78 78
       -d, --daemon=false: Enable daemon mode
79 79
       --dns=[]: Force docker to use specific DNS servers
80
+      --dns-search=[]: Force Docker to use specific DNS search domains
80 81
       -g, --graph="/var/lib/docker": Path to use as the root of the docker runtime
81 82
       --icc=true: Enable inter-container communication
82 83
       --ip="0.0.0.0": Default IP address to use when binding container ports
... ...
@@ -96,6 +97,8 @@ To force Docker to use devicemapper as the storage driver, use ``docker -d -s de
96 96
 
97 97
 To set the DNS server for all Docker containers, use ``docker -d --dns 8.8.8.8``.
98 98
 
99
+To set the a DNS search domain for all Docker containers, use ``docker -d --dns-search example.com``.
100
+
99 101
 To run the daemon with debug output, use ``docker -d -D``.
100 102
 
101 103
 To use lxc as the execution driver, use ``docker -d -e lxc``.
... ...
@@ -396,6 +399,7 @@ not overridden in the JSON hash will be merged in.
396 396
       "VolumesFrom" : "",
397 397
       "Cmd" : ["cat", "-e", "/etc/resolv.conf"],
398 398
       "Dns" : ["8.8.8.8", "8.8.4.4"],
399
+      "DnsSearch" : ["example.com"],
399 400
       "MemorySwap" : 0,
400 401
       "AttachStdin" : false,
401 402
       "AttachStderr" : false,
... ...
@@ -1131,6 +1135,7 @@ image is removed.
1131 1131
       -t, --tty=false: Allocate a pseudo-tty
1132 1132
       -u, --user="": Username or UID
1133 1133
       --dns=[]: Set custom dns servers for the container
1134
+      --dns-search=[]: Set custom DNS search domains for the container
1134 1135
       -v, --volume=[]: Create a bind mount to a directory or file with: [host-path]:[container-path]:[rw|ro]. If a directory "container-path" is missing, then docker creates a new volume.
1135 1136
       --volumes-from="": Mount all volumes from the given container(s)
1136 1137
       --entrypoint="": Overwrite the default entrypoint set by the image
... ...
@@ -1288,7 +1293,7 @@ A complete example
1288 1288
    $ sudo docker run -d --name static static-web-files sh
1289 1289
    $ sudo docker run -d --expose=8098 --name riak riakserver
1290 1290
    $ sudo docker run -d -m 100m -e DEVELOPMENT=1 -e BRANCH=example-code -v $(pwd):/app/bin:ro --name app appserver
1291
-   $ sudo docker run -d -p 1443:443 --dns=dns.dev.org -v /var/log/httpd --volumes-from static --link riak --link app -h www.sven.dev.org --name web webserver
1291
+   $ sudo docker run -d -p 1443:443 --dns=dns.dev.org --dns-search=dev.org -v /var/log/httpd --volumes-from static --link riak --link app -h www.sven.dev.org --name web webserver
1292 1292
    $ sudo docker run -t -i --rm --volumes-from web -w /var/log/httpd busybox tail -f access.log
1293 1293
 
1294 1294
 This example shows 5 containers that might be set up to test a web application change:
... ...
@@ -1296,7 +1301,7 @@ This example shows 5 containers that might be set up to test a web application c
1296 1296
 1. Start a pre-prepared volume image ``static-web-files`` (in the background) that has CSS, image and static HTML in it, (with a ``VOLUME`` instruction in the ``Dockerfile`` to allow the web server to use those files);
1297 1297
 2. Start a pre-prepared ``riakserver`` image, give the container name ``riak`` and expose port ``8098`` to any containers that link to it;
1298 1298
 3. Start the ``appserver`` image, restricting its memory usage to 100MB, setting two environment variables ``DEVELOPMENT`` and ``BRANCH`` and bind-mounting the current directory (``$(pwd)``) in the container in read-only mode as ``/app/bin``;
1299
-4. Start the ``webserver``, mapping port ``443`` in the container to port ``1443`` on the Docker server, setting the DNS server to ``dns.dev.org``, creating a volume to put the log files into (so we can access it from another container), then importing the files from the volume exposed by the ``static`` container, and linking to all exposed ports from ``riak`` and ``app``. Lastly, we set the hostname to ``web.sven.dev.org`` so its consistent with the pre-generated SSL certificate;
1299
+4. Start the ``webserver``, mapping port ``443`` in the container to port ``1443`` on the Docker server, setting the DNS server to ``dns.dev.org`` and DNS search domain to ``dev.org``, creating a volume to put the log files into (so we can access it from another container), then importing the files from the volume exposed by the ``static`` container, and linking to all exposed ports from ``riak`` and ``app``. Lastly, we set the hostname to ``web.sven.dev.org`` so its consistent with the pre-generated SSL certificate;
1300 1300
 5. Finally, we create a container that runs ``tail -f access.log`` using the logs volume from the ``web`` container, setting the workdir to ``/var/log/httpd``. The ``--rm`` option means that when the container exits, the container's layer is removed.
1301 1301
 
1302 1302
 
... ...
@@ -136,3 +136,16 @@ func ValidateIp4Address(val string) (string, error) {
136 136
 	}
137 137
 	return "", fmt.Errorf("%s is not an ip4 address", val)
138 138
 }
139
+
140
+func ValidateDomain(val string) (string, error) {
141
+	alpha := regexp.MustCompile(`[a-zA-Z]`)
142
+	if alpha.FindString(val) == "" {
143
+		return "", fmt.Errorf("%s is not a valid domain", val)
144
+	}
145
+	re := regexp.MustCompile(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`)
146
+	var ns = re.FindSubmatch([]byte(val))
147
+	if len(ns) > 0 {
148
+		return string(ns[1]), nil
149
+	}
150
+	return "", fmt.Errorf("%s is not a valid domain", val)
151
+}
... ...
@@ -22,3 +22,57 @@ func TestValidateIP4(t *testing.T) {
22 22
 	}
23 23
 
24 24
 }
25
+
26
+func TestValidateDomain(t *testing.T) {
27
+	valid := []string{
28
+		`a`,
29
+		`a.`,
30
+		`1.foo`,
31
+		`17.foo`,
32
+		`foo.bar`,
33
+		`foo.bar.baz`,
34
+		`foo.bar.`,
35
+		`foo.bar.baz`,
36
+		`foo1.bar2`,
37
+		`foo1.bar2.baz`,
38
+		`1foo.2bar.`,
39
+		`1foo.2bar.baz`,
40
+		`foo-1.bar-2`,
41
+		`foo-1.bar-2.baz`,
42
+		`foo-1.bar-2.`,
43
+		`foo-1.bar-2.baz`,
44
+		`1-foo.2-bar`,
45
+		`1-foo.2-bar.baz`,
46
+		`1-foo.2-bar.`,
47
+		`1-foo.2-bar.baz`,
48
+	}
49
+
50
+	invalid := []string{
51
+		``,
52
+		`.`,
53
+		`17`,
54
+		`17.`,
55
+		`.17`,
56
+		`17-.`,
57
+		`17-.foo`,
58
+		`.foo`,
59
+		`foo-.bar`,
60
+		`-foo.bar`,
61
+		`foo.bar-`,
62
+		`foo.bar-.baz`,
63
+		`foo.-bar`,
64
+		`foo.-bar.baz`,
65
+	}
66
+
67
+	for _, domain := range valid {
68
+		if ret, err := ValidateDomain(domain); err != nil || ret == "" {
69
+			t.Fatalf("ValidateDomain(`"+domain+"`) got %s %s", ret, err)
70
+		}
71
+	}
72
+
73
+	for _, domain := range invalid {
74
+		if ret, err := ValidateDomain(domain); err == nil || ret != "" {
75
+			t.Fatalf("ValidateDomain(`"+domain+"`) got %s %s", ret, err)
76
+		}
77
+	}
78
+}
... ...
@@ -20,6 +20,7 @@ func Compare(a, b *Config) bool {
20 20
 	}
21 21
 	if len(a.Cmd) != len(b.Cmd) ||
22 22
 		len(a.Dns) != len(b.Dns) ||
23
+		len(a.DnsSearch) != len(b.DnsSearch) ||
23 24
 		len(a.Env) != len(b.Env) ||
24 25
 		len(a.PortSpecs) != len(b.PortSpecs) ||
25 26
 		len(a.ExposedPorts) != len(b.ExposedPorts) ||
... ...
@@ -38,6 +39,11 @@ func Compare(a, b *Config) bool {
38 38
 			return false
39 39
 		}
40 40
 	}
41
+	for i := 0; i < len(a.DnsSearch); i++ {
42
+		if a.DnsSearch[i] != b.DnsSearch[i] {
43
+			return false
44
+		}
45
+	}
41 46
 	for i := 0; i < len(a.Env); i++ {
42 47
 		if a.Env[i] != b.Env[i] {
43 48
 			return false
... ...
@@ -26,6 +26,7 @@ type Config struct {
26 26
 	Env             []string
27 27
 	Cmd             []string
28 28
 	Dns             []string
29
+	DnsSearch       []string
29 30
 	Image           string // Name of the image as it was passed by the operator (eg. could be symbolic)
30 31
 	Volumes         map[string]struct{}
31 32
 	VolumesFrom     string
... ...
@@ -68,6 +69,9 @@ func ContainerConfigFromJob(job *engine.Job) *Config {
68 68
 	if Dns := job.GetenvList("Dns"); Dns != nil {
69 69
 		config.Dns = Dns
70 70
 	}
71
+	if DnsSearch := job.GetenvList("DnsSearch"); DnsSearch != nil {
72
+		config.DnsSearch = DnsSearch
73
+	}
71 74
 	if Entrypoint := job.GetenvList("Entrypoint"); Entrypoint != nil {
72 75
 		config.Entrypoint = Entrypoint
73 76
 	}
... ...
@@ -164,6 +164,7 @@ func TestCompare(t *testing.T) {
164 164
 	volumes1["/test1"] = struct{}{}
165 165
 	config1 := Config{
166 166
 		Dns:         []string{"1.1.1.1", "2.2.2.2"},
167
+		DnsSearch:   []string{"foo", "bar"},
167 168
 		PortSpecs:   []string{"1111:1111", "2222:2222"},
168 169
 		Env:         []string{"VAR1=1", "VAR2=2"},
169 170
 		VolumesFrom: "11111111",
... ...
@@ -171,6 +172,7 @@ func TestCompare(t *testing.T) {
171 171
 	}
172 172
 	config2 := Config{
173 173
 		Dns:         []string{"0.0.0.0", "2.2.2.2"},
174
+		DnsSearch:   []string{"foo", "bar"},
174 175
 		PortSpecs:   []string{"1111:1111", "2222:2222"},
175 176
 		Env:         []string{"VAR1=1", "VAR2=2"},
176 177
 		VolumesFrom: "11111111",
... ...
@@ -178,6 +180,7 @@ func TestCompare(t *testing.T) {
178 178
 	}
179 179
 	config3 := Config{
180 180
 		Dns:         []string{"1.1.1.1", "2.2.2.2"},
181
+		DnsSearch:   []string{"foo", "bar"},
181 182
 		PortSpecs:   []string{"0000:0000", "2222:2222"},
182 183
 		Env:         []string{"VAR1=1", "VAR2=2"},
183 184
 		VolumesFrom: "11111111",
... ...
@@ -185,6 +188,7 @@ func TestCompare(t *testing.T) {
185 185
 	}
186 186
 	config4 := Config{
187 187
 		Dns:         []string{"1.1.1.1", "2.2.2.2"},
188
+		DnsSearch:   []string{"foo", "bar"},
188 189
 		PortSpecs:   []string{"0000:0000", "2222:2222"},
189 190
 		Env:         []string{"VAR1=1", "VAR2=2"},
190 191
 		VolumesFrom: "22222222",
... ...
@@ -194,11 +198,20 @@ func TestCompare(t *testing.T) {
194 194
 	volumes2["/test2"] = struct{}{}
195 195
 	config5 := Config{
196 196
 		Dns:         []string{"1.1.1.1", "2.2.2.2"},
197
+		DnsSearch:   []string{"foo", "bar"},
197 198
 		PortSpecs:   []string{"0000:0000", "2222:2222"},
198 199
 		Env:         []string{"VAR1=1", "VAR2=2"},
199 200
 		VolumesFrom: "11111111",
200 201
 		Volumes:     volumes2,
201 202
 	}
203
+	config6 := Config{
204
+		Dns:         []string{"1.1.1.1", "2.2.2.2"},
205
+		DnsSearch:   []string{"foos", "bars"},
206
+		PortSpecs:   []string{"1111:1111", "2222:2222"},
207
+		Env:         []string{"VAR1=1", "VAR2=2"},
208
+		VolumesFrom: "11111111",
209
+		Volumes:     volumes1,
210
+	}
202 211
 	if Compare(&config1, &config2) {
203 212
 		t.Fatalf("Compare should return false, Dns are different")
204 213
 	}
... ...
@@ -211,6 +224,9 @@ func TestCompare(t *testing.T) {
211 211
 	if Compare(&config1, &config5) {
212 212
 		t.Fatalf("Compare should return false, Volumes are different")
213 213
 	}
214
+	if Compare(&config1, &config6) {
215
+		t.Fatalf("Compare should return false, DnsSearch are different")
216
+	}
214 217
 	if !Compare(&config1, &config1) {
215 218
 		t.Fatalf("Compare should return true")
216 219
 	}
... ...
@@ -100,6 +100,12 @@ func Merge(userConf, imageConf *Config) error {
100 100
 		//duplicates aren't an issue here
101 101
 		userConf.Dns = append(userConf.Dns, imageConf.Dns...)
102 102
 	}
103
+	if userConf.DnsSearch == nil || len(userConf.DnsSearch) == 0 {
104
+		userConf.DnsSearch = imageConf.DnsSearch
105
+	} else {
106
+		//duplicates aren't an issue here
107
+		userConf.DnsSearch = append(userConf.DnsSearch, imageConf.DnsSearch...)
108
+	}
103 109
 	if userConf.Entrypoint == nil || len(userConf.Entrypoint) == 0 {
104 110
 		userConf.Entrypoint = imageConf.Entrypoint
105 111
 	}
... ...
@@ -42,6 +42,7 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf
42 42
 		flPublish     opts.ListOpts
43 43
 		flExpose      opts.ListOpts
44 44
 		flDns         opts.ListOpts
45
+		flDnsSearch   = opts.NewListOpts(opts.ValidateDomain)
45 46
 		flVolumesFrom opts.ListOpts
46 47
 		flLxcOpts     opts.ListOpts
47 48
 
... ...
@@ -73,6 +74,7 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf
73 73
 	cmd.Var(&flPublish, []string{"p", "-publish"}, fmt.Sprintf("Publish a container's port to the host (format: %s) (use 'docker port' to see the actual mapping)", nat.PortSpecTemplateFormat))
74 74
 	cmd.Var(&flExpose, []string{"#expose", "-expose"}, "Expose a port from the container without publishing it to your host")
75 75
 	cmd.Var(&flDns, []string{"#dns", "-dns"}, "Set custom dns servers")
76
+	cmd.Var(&flDnsSearch, []string{"-dns-search"}, "Set custom dns search domains")
76 77
 	cmd.Var(&flVolumesFrom, []string{"#volumes-from", "-volumes-from"}, "Mount volumes from the specified container(s)")
77 78
 	cmd.Var(&flLxcOpts, []string{"#lxc-conf", "-lxc-conf"}, "Add custom lxc options --lxc-conf=\"lxc.cgroup.cpuset.cpus = 0,1\"")
78 79
 
... ...
@@ -196,6 +198,7 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf
196 196
 		Env:             flEnv.GetAll(),
197 197
 		Cmd:             runCmd,
198 198
 		Dns:             flDns.GetAll(),
199
+		DnsSearch:       flDnsSearch.GetAll(),
199 200
 		Image:           image,
200 201
 		Volumes:         flVolumes.GetMap(),
201 202
 		VolumesFrom:     strings.Join(flVolumesFrom.GetAll(), ","),
... ...
@@ -493,13 +493,19 @@ func (runtime *Runtime) Create(config *runconfig.Config, name string) (*Containe
493 493
 	}
494 494
 
495 495
 	// If custom dns exists, then create a resolv.conf for the container
496
-	if len(config.Dns) > 0 || len(runtime.config.Dns) > 0 {
497
-		var dns []string
496
+	if len(config.Dns) > 0 || len(runtime.config.Dns) > 0 || len(config.DnsSearch) > 0 || len(runtime.config.DnsSearch) > 0 {
497
+		dns := utils.GetNameservers(resolvConf)
498
+		dnsSearch := utils.GetSearchDomains(resolvConf)
498 499
 		if len(config.Dns) > 0 {
499 500
 			dns = config.Dns
500
-		} else {
501
+		} else if len(runtime.config.Dns) > 0 {
501 502
 			dns = runtime.config.Dns
502 503
 		}
504
+		if len(config.DnsSearch) > 0 {
505
+			dnsSearch = config.DnsSearch
506
+		} else if len(runtime.config.DnsSearch) > 0 {
507
+			dnsSearch = runtime.config.DnsSearch
508
+		}
503 509
 		container.ResolvConfPath = path.Join(container.root, "resolv.conf")
504 510
 		f, err := os.Create(container.ResolvConfPath)
505 511
 		if err != nil {
... ...
@@ -511,6 +517,11 @@ func (runtime *Runtime) Create(config *runconfig.Config, name string) (*Containe
511 511
 				return nil, nil, err
512 512
 			}
513 513
 		}
514
+		if len(dnsSearch) > 0 {
515
+			if _, err := f.Write([]byte("search " + strings.Join(dnsSearch, " ") + "\n")); err != nil {
516
+				return nil, nil, err
517
+			}
518
+		}
514 519
 	} else {
515 520
 		container.ResolvConfPath = "/etc/resolv.conf"
516 521
 	}
... ...
@@ -731,54 +731,78 @@ func GetResolvConf() ([]byte, error) {
731 731
 // CheckLocalDns looks into the /etc/resolv.conf,
732 732
 // it returns true if there is a local nameserver or if there is no nameserver.
733 733
 func CheckLocalDns(resolvConf []byte) bool {
734
-	var parsedResolvConf = StripComments(resolvConf, []byte("#"))
735
-	if !bytes.Contains(parsedResolvConf, []byte("nameserver")) {
736
-		return true
737
-	}
738
-	for _, ip := range [][]byte{
739
-		[]byte("127.0.0.1"),
740
-		[]byte("127.0.1.1"),
741
-	} {
742
-		if bytes.Contains(parsedResolvConf, ip) {
743
-			return true
734
+	for _, line := range GetLines(resolvConf, []byte("#")) {
735
+		if !bytes.Contains(line, []byte("nameserver")) {
736
+			continue
737
+		}
738
+		for _, ip := range [][]byte{
739
+			[]byte("127.0.0.1"),
740
+			[]byte("127.0.1.1"),
741
+		} {
742
+			if bytes.Contains(line, ip) {
743
+				return true
744
+			}
744 745
 		}
746
+		return false
745 747
 	}
746
-	return false
748
+	return true
747 749
 }
748 750
 
749
-// StripComments parses input into lines and strips away comments.
750
-func StripComments(input []byte, commentMarker []byte) []byte {
751
+// GetLines parses input into lines and strips away comments.
752
+func GetLines(input []byte, commentMarker []byte) [][]byte {
751 753
 	lines := bytes.Split(input, []byte("\n"))
752
-	var output []byte
754
+	var output [][]byte
753 755
 	for _, currentLine := range lines {
754 756
 		var commentIndex = bytes.Index(currentLine, commentMarker)
755 757
 		if commentIndex == -1 {
756
-			output = append(output, currentLine...)
758
+			output = append(output, currentLine)
757 759
 		} else {
758
-			output = append(output, currentLine[:commentIndex]...)
760
+			output = append(output, currentLine[:commentIndex])
759 761
 		}
760
-		output = append(output, []byte("\n")...)
761 762
 	}
762 763
 	return output
763 764
 }
764 765
 
765
-// GetNameserversAsCIDR returns nameservers (if any) listed in
766
-// /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32")
767
-// This function's output is intended for net.ParseCIDR
768
-func GetNameserversAsCIDR(resolvConf []byte) []string {
769
-	var parsedResolvConf = StripComments(resolvConf, []byte("#"))
766
+// GetNameservers returns nameservers (if any) listed in /etc/resolv.conf
767
+func GetNameservers(resolvConf []byte) []string {
770 768
 	nameservers := []string{}
771 769
 	re := regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`)
772
-	for _, line := range bytes.Split(parsedResolvConf, []byte("\n")) {
770
+	for _, line := range GetLines(resolvConf, []byte("#")) {
773 771
 		var ns = re.FindSubmatch(line)
774 772
 		if len(ns) > 0 {
775
-			nameservers = append(nameservers, string(ns[1])+"/32")
773
+			nameservers = append(nameservers, string(ns[1]))
776 774
 		}
777 775
 	}
776
+	return nameservers
777
+}
778 778
 
779
+// GetNameserversAsCIDR returns nameservers (if any) listed in
780
+// /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32")
781
+// This function's output is intended for net.ParseCIDR
782
+func GetNameserversAsCIDR(resolvConf []byte) []string {
783
+	nameservers := []string{}
784
+	for _, nameserver := range GetNameservers(resolvConf) {
785
+		nameservers = append(nameservers, nameserver+"/32")
786
+	}
779 787
 	return nameservers
780 788
 }
781 789
 
790
+// GetSearchDomains returns search domains (if any) listed in /etc/resolv.conf
791
+// If more than one search line is encountered, only the contents of the last
792
+// one is returned.
793
+func GetSearchDomains(resolvConf []byte) []string {
794
+	re := regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`)
795
+	domains := []string{}
796
+	for _, line := range GetLines(resolvConf, []byte("#")) {
797
+		match := re.FindSubmatch(line)
798
+		if match == nil {
799
+			continue
800
+		}
801
+		domains = strings.Fields(string(match[1]))
802
+	}
803
+	return domains
804
+}
805
+
782 806
 // FIXME: Change this not to receive default value as parameter
783 807
 func ParseHost(defaultHost string, defaultUnix, addr string) (string, error) {
784 808
 	var (
... ...
@@ -444,6 +444,30 @@ func TestParsePortMapping(t *testing.T) {
444 444
 	}
445 445
 }
446 446
 
447
+func TestGetNameservers(t *testing.T) {
448
+	for resolv, result := range map[string][]string{`
449
+nameserver 1.2.3.4
450
+nameserver 40.3.200.10
451
+search example.com`: {"1.2.3.4", "40.3.200.10"},
452
+		`search example.com`: {},
453
+		`nameserver 1.2.3.4
454
+search example.com
455
+nameserver 4.30.20.100`: {"1.2.3.4", "4.30.20.100"},
456
+		``: {},
457
+		`  nameserver 1.2.3.4   `: {"1.2.3.4"},
458
+		`search example.com
459
+nameserver 1.2.3.4
460
+#nameserver 4.3.2.1`: {"1.2.3.4"},
461
+		`search example.com
462
+nameserver 1.2.3.4 # not 4.3.2.1`: {"1.2.3.4"},
463
+	} {
464
+		test := GetNameservers([]byte(resolv))
465
+		if !StrSlicesEqual(test, result) {
466
+			t.Fatalf("Wrong nameserver string {%s} should be %v. Input: %s", test, result, resolv)
467
+		}
468
+	}
469
+}
470
+
447 471
 func TestGetNameserversAsCIDR(t *testing.T) {
448 472
 	for resolv, result := range map[string][]string{`
449 473
 nameserver 1.2.3.4
... ...
@@ -468,6 +492,33 @@ nameserver 1.2.3.4 # not 4.3.2.1`: {"1.2.3.4/32"},
468 468
 	}
469 469
 }
470 470
 
471
+func TestGetSearchDomains(t *testing.T) {
472
+	for resolv, result := range map[string][]string{
473
+		`search example.com`:           {"example.com"},
474
+		`search example.com # ignored`: {"example.com"},
475
+		` 	  search 	 example.com 	  `: {"example.com"},
476
+		` 	  search 	 example.com 	  # ignored`: {"example.com"},
477
+		`search foo.example.com example.com`: {"foo.example.com", "example.com"},
478
+		`	   search   	   foo.example.com 	 example.com 	`: {"foo.example.com", "example.com"},
479
+		`	   search   	   foo.example.com 	 example.com 	# ignored`: {"foo.example.com", "example.com"},
480
+		``:          {},
481
+		`# ignored`: {},
482
+		`nameserver 1.2.3.4
483
+search foo.example.com example.com`: {"foo.example.com", "example.com"},
484
+		`nameserver 1.2.3.4
485
+search dup1.example.com dup2.example.com
486
+search foo.example.com example.com`: {"foo.example.com", "example.com"},
487
+		`nameserver 1.2.3.4
488
+search foo.example.com example.com
489
+nameserver 4.30.20.100`: {"foo.example.com", "example.com"},
490
+	} {
491
+		test := GetSearchDomains([]byte(resolv))
492
+		if !StrSlicesEqual(test, result) {
493
+			t.Fatalf("Wrong search domain string {%s} should be %v. Input: %s", test, result, resolv)
494
+		}
495
+	}
496
+}
497
+
471 498
 func StrSlicesEqual(a, b []string) bool {
472 499
 	if len(a) != len(b) {
473 500
 		return false