mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-30 17:53:21 -04:00 
			
		
		
		
	Add inline support for middleware/hosts (#1072)
This fix add inline support for middleware/hosts so that
it is possible to specify hosts file insides the Corefile:
```
hosts inline example.org {
    10.0.0.1 example.org
    fallthrough
}
```
This fix fixes 999.
Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
			
			
This commit is contained in:
		| @@ -11,6 +11,7 @@ available hosts files that block access to advertising servers. | |||||||
|  |  | ||||||
| ~~~ | ~~~ | ||||||
| hosts [FILE [ZONES...]] { | hosts [FILE [ZONES...]] { | ||||||
|  |     [INLINE] | ||||||
|     fallthrough |     fallthrough | ||||||
| } | } | ||||||
| ~~~ | ~~~ | ||||||
| @@ -18,7 +19,10 @@ hosts [FILE [ZONES...]] { | |||||||
| * **FILE** the hosts file to read and parse. If the path is relative the path from the *root* | * **FILE** the hosts file to read and parse. If the path is relative the path from the *root* | ||||||
|   directive will be prepended to it. Defaults to /etc/hosts if omitted |   directive will be prepended to it. Defaults to /etc/hosts if omitted | ||||||
| * **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block | * **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block | ||||||
|     are used. |    are used. | ||||||
|  | * **INLINE** the hosts file contents inlined in Corefile. If there are any lines before fallthrough | ||||||
|  |    then all of them will be treated as the additional content for hosts file. The specified hosts | ||||||
|  |    file path will still be read but entries will be overrided. | ||||||
| * `fallthrough` If zone matches and no record can be generated, pass request to the next plugin. | * `fallthrough` If zone matches and no record can be generated, pass request to the next plugin. | ||||||
|  |  | ||||||
| ## Examples | ## Examples | ||||||
| @@ -43,3 +47,12 @@ hosts example.hosts example.org example.net { | |||||||
|     fallthrough |     fallthrough | ||||||
| } | } | ||||||
| ~~~ | ~~~ | ||||||
|  |  | ||||||
|  | Load hosts file inlined in Corefile. | ||||||
|  |  | ||||||
|  | ~~~ | ||||||
|  | hosts example.hosts example.org { | ||||||
|  |     10.0.0.1 example.org | ||||||
|  |     fallthrough | ||||||
|  | } | ||||||
|  | ~~~ | ||||||
|   | |||||||
| @@ -53,6 +53,11 @@ type Hostsfile struct { | |||||||
| 	// We don't support old-classful IP address notation. | 	// We don't support old-classful IP address notation. | ||||||
| 	byAddr map[string][]string | 	byAddr map[string][]string | ||||||
|  |  | ||||||
|  | 	// inline saves the hosts file is inlined in Corefile | ||||||
|  | 	// We need a copy here as we want to use inline to override | ||||||
|  | 	// the default /etc/hosts | ||||||
|  | 	inline []string | ||||||
|  |  | ||||||
| 	expire time.Time | 	expire time.Time | ||||||
| 	path   string | 	path   string | ||||||
| 	mtime  time.Time | 	mtime  time.Time | ||||||
| @@ -74,6 +79,10 @@ func (h *Hostsfile) ReadHosts() { | |||||||
|  |  | ||||||
| 	var file *os.File | 	var file *os.File | ||||||
| 	if file, _ = os.Open(h.path); file == nil { | 	if file, _ = os.Open(h.path); file == nil { | ||||||
|  | 		// If this is the first time then we will try to parse inline | ||||||
|  | 		if len(h.byAddr) == 0 && len(h.inline) > 0 { | ||||||
|  | 			h.Parse(nil) | ||||||
|  | 		} | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	defer file.Close() | 	defer file.Close() | ||||||
| @@ -92,7 +101,12 @@ func (h *Hostsfile) Parse(file io.Reader) { | |||||||
| 	hsv6 := make(map[string][]net.IP) | 	hsv6 := make(map[string][]net.IP) | ||||||
| 	is := make(map[string][]string) | 	is := make(map[string][]string) | ||||||
|  |  | ||||||
| 	scanner := bufio.NewScanner(file) | 	var readers []io.Reader | ||||||
|  | 	if file != nil { | ||||||
|  | 		readers = append(readers, file) | ||||||
|  | 	} | ||||||
|  | 	readers = append(readers, strings.NewReader(strings.Join(h.inline, "\n"))) | ||||||
|  | 	scanner := bufio.NewScanner(io.MultiReader(readers...)) | ||||||
| 	for scanner.Scan() { | 	for scanner.Scan() { | ||||||
| 		line := scanner.Bytes() | 		line := scanner.Bytes() | ||||||
| 		if i := bytes.Index(line, []byte{'#'}); i >= 0 { | 		if i := bytes.Index(line, []byte{'#'}); i >= 0 { | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import ( | |||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/coredns/coredns/core/dnsserver" | 	"github.com/coredns/coredns/core/dnsserver" | ||||||
| 	"github.com/coredns/coredns/plugin" | 	"github.com/coredns/coredns/plugin" | ||||||
| @@ -80,6 +81,11 @@ func hostsParse(c *caddy.Controller) (Hosts, error) { | |||||||
| 				} | 				} | ||||||
| 				return h, c.ArgErr() | 				return h, c.ArgErr() | ||||||
| 			default: | 			default: | ||||||
|  | 				if !h.Fallthrough { | ||||||
|  | 					line := strings.Join(append([]string{c.Val()}, c.RemainingArgs()...), " ") | ||||||
|  | 					h.inline = append(h.inline, line) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
| 				return h, c.Errf("unknown property '%s'", c.Val()) | 				return h, c.Errf("unknown property '%s'", c.Val()) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -84,3 +84,77 @@ func TestHostsParse(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestHostsInlineParse(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		inputFileRules      string | ||||||
|  | 		shouldErr           bool | ||||||
|  | 		expectedbyAddr      map[string][]string | ||||||
|  | 		expectedFallthrough bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			`hosts highly_unlikely_to_exist_hosts_file example.org { | ||||||
|  |                                 10.0.0.1 example.org | ||||||
|  |                                 fallthrough | ||||||
|  |                         }`, | ||||||
|  | 			false, | ||||||
|  | 			map[string][]string{ | ||||||
|  | 				`10.0.0.1`: { | ||||||
|  | 					`example.org.`, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			`hosts highly_unlikely_to_exist_hosts_file example.org { | ||||||
|  |                                 10.0.0.1 example.org | ||||||
|  |                         }`, | ||||||
|  | 			false, | ||||||
|  | 			map[string][]string{ | ||||||
|  | 				`10.0.0.1`: { | ||||||
|  | 					`example.org.`, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			`hosts highly_unlikely_to_exist_hosts_file example.org { | ||||||
|  |                                 fallthrough | ||||||
|  |                                 10.0.0.1 example.org | ||||||
|  |                         }`, | ||||||
|  | 			true, | ||||||
|  | 			map[string][]string{}, | ||||||
|  | 			true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i, test := range tests { | ||||||
|  | 		c := caddy.NewTestController("dns", test.inputFileRules) | ||||||
|  | 		h, err := hostsParse(c) | ||||||
|  |  | ||||||
|  | 		if err == nil && test.shouldErr { | ||||||
|  | 			t.Fatalf("Test %d expected errors, but got no error", i) | ||||||
|  | 		} else if err != nil && !test.shouldErr { | ||||||
|  | 			t.Fatalf("Test %d expected no errors, but got '%v'", i, err) | ||||||
|  | 		} else if !test.shouldErr { | ||||||
|  | 			if h.Fallthrough != test.expectedFallthrough { | ||||||
|  | 				t.Fatalf("Test %d expected fallthrough of %v, got %v", i, test.expectedFallthrough, h.Fallthrough) | ||||||
|  | 			} | ||||||
|  | 			for k, expectedVal := range test.expectedbyAddr { | ||||||
|  | 				if val, ok := h.byAddr[k]; !ok { | ||||||
|  | 					t.Fatalf("Test %d expected %v, got no entry", i, k) | ||||||
|  | 				} else { | ||||||
|  | 					if len(expectedVal) != len(val) { | ||||||
|  | 						t.Fatalf("Test %d expected %v records for %v, got %v", i, len(expectedVal), k, len(val)) | ||||||
|  | 					} | ||||||
|  | 					for j := range expectedVal { | ||||||
|  | 						if expectedVal[j] != val[j] { | ||||||
|  | 							t.Fatalf("Test %d expected %v for %v, got %v", i, expectedVal[j], j, val[j]) | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										48
									
								
								test/hosts_file_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								test/hosts_file_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | package test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/coredns/coredns/plugin/proxy" | ||||||
|  | 	"github.com/coredns/coredns/plugin/test" | ||||||
|  | 	"github.com/coredns/coredns/request" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestHostsInlineLookup(t *testing.T) { | ||||||
|  | 	corefile := `example.org:0 { | ||||||
|  |                        hosts highly_unlikely_to_exist_hosts_file example.org { | ||||||
|  |                          10.0.0.1 example.org | ||||||
|  |                          fallthrough | ||||||
|  |                       }	 | ||||||
|  |                     }` | ||||||
|  |  | ||||||
|  | 	i, udp, _, err := CoreDNSServerAndPorts(corefile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Could not get CoreDNS serving instance: %s", err) | ||||||
|  | 	} | ||||||
|  | 	defer i.Stop() | ||||||
|  |  | ||||||
|  | 	log.SetOutput(ioutil.Discard) | ||||||
|  |  | ||||||
|  | 	p := proxy.NewLookup([]string{udp}) | ||||||
|  | 	state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} | ||||||
|  |  | ||||||
|  | 	resp, err := p.Lookup(state, "example.org.", dns.TypeA) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("Expected to receive reply, but didn't") | ||||||
|  | 	} | ||||||
|  | 	// expect answer section with A record in it | ||||||
|  | 	if len(resp.Answer) == 0 { | ||||||
|  | 		t.Fatal("Expected to at least one RR in the answer section, got none") | ||||||
|  | 	} | ||||||
|  | 	if resp.Answer[0].Header().Rrtype != dns.TypeA { | ||||||
|  | 		t.Errorf("Expected RR to A, got: %d", resp.Answer[0].Header().Rrtype) | ||||||
|  | 	} | ||||||
|  | 	if resp.Answer[0].(*dns.A).A.String() != "10.0.0.1" { | ||||||
|  | 		t.Errorf("Expected 10.0.0.1, got: %s", resp.Answer[0].(*dns.A).A.String()) | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user