From b66e370ffc7be134153714d5e7e74e70178264a6 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Tue, 23 Jul 2019 11:43:30 +0300
Subject: [PATCH 1/6] * dnsfilter: refactor: a simple approach to convert
 Reason to string

---
 dnsfilter/dnsfilter.go     | 17 +++++++++++++++++
 dnsfilter/reason_string.go | 16 ----------------
 2 files changed, 17 insertions(+), 16 deletions(-)
 delete mode 100644 dnsfilter/reason_string.go

diff --git a/dnsfilter/dnsfilter.go b/dnsfilter/dnsfilter.go
index bc03e448..a642abe4 100644
--- a/dnsfilter/dnsfilter.go
+++ b/dnsfilter/dnsfilter.go
@@ -133,6 +133,23 @@ const (
 	FilteredSafeSearch
 )
 
+func (i Reason) String() string {
+	names := []string{
+		"NotFilteredNotFound",
+		"NotFilteredWhiteList",
+		"NotFilteredError",
+		"FilteredBlackList",
+		"FilteredSafeBrowsing",
+		"FilteredParental",
+		"FilteredInvalid",
+		"FilteredSafeSearch",
+	}
+	if uint(i) >= uint(len(names)) {
+		return ""
+	}
+	return names[i]
+}
+
 type dnsFilterContext struct {
 	stats             Stats
 	dialCache         gcache.Cache // "host" -> "IP" cache for safebrowsing and parental control servers
diff --git a/dnsfilter/reason_string.go b/dnsfilter/reason_string.go
deleted file mode 100644
index cb097e52..00000000
--- a/dnsfilter/reason_string.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Code generated by "stringer -type=Reason"; DO NOT EDIT.
-
-package dnsfilter
-
-import "strconv"
-
-const _Reason_name = "NotFilteredNotFoundNotFilteredWhiteListNotFilteredErrorFilteredBlackListFilteredSafeBrowsingFilteredParentalFilteredInvalidFilteredSafeSearch"
-
-var _Reason_index = [...]uint8{0, 19, 39, 55, 72, 92, 108, 123, 141}
-
-func (i Reason) String() string {
-	if i < 0 || i >= Reason(len(_Reason_index)-1) {
-		return "Reason(" + strconv.FormatInt(int64(i), 10) + ")"
-	}
-	return _Reason_name[_Reason_index[i]:_Reason_index[i+1]]
-}

From 0c2459b51bfb76a767ee58cc8af859d9063cfcb9 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Thu, 18 Jul 2019 19:16:04 +0300
Subject: [PATCH 2/6] + doc: add Rewrites section

---
 AGHTechDoc.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 61 insertions(+)

diff --git a/AGHTechDoc.md b/AGHTechDoc.md
index cfe0304f..bd526fc8 100644
--- a/AGHTechDoc.md
+++ b/AGHTechDoc.md
@@ -27,6 +27,10 @@ Contents:
 * DNS access settings
 	* List access settings
 	* Set access settings
+* Rewrites
+	* API: List rewrite entries
+	* API: Add a rewrite entry
+	* API: Remove a rewrite entry
 
 
 ## First startup
@@ -682,3 +686,60 @@ Request:
 Response:
 
 	200 OK
+
+
+## Rewrites
+
+This section allows the administrator to easily configure custom DNS response for a specific domain name.
+A, AAAA and CNAME records are supported.
+
+
+### API: List rewrite entries
+
+Request:
+
+	GET /control/rewrite/list
+
+Response:
+
+	200 OK
+
+	[
+	{
+		domain: "..."
+		answer: "..."
+	}
+	...
+	]
+
+
+### API: Add a rewrite entry
+
+Request:
+
+	POST /control/rewrite/add
+
+	{
+		domain: "..."
+		answer: "..." // "1.2.3.4" (A) || "::1" (AAAA) || "hostname" (CNAME)
+	}
+
+Response:
+
+	200 OK
+
+
+### API: Remove a rewrite entry
+
+Request:
+
+	POST /control/rewrite/delete
+
+	{
+		domain: "..."
+		answer: "..."
+	}
+
+Response:
+
+	200 OK

From 9857024c5dc3d8db9becceae7d7231b105bec2b4 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Fri, 19 Jul 2019 15:14:19 +0300
Subject: [PATCH 3/6] + control: add /rewrite/* handlers

---
 home/control.go      |   1 +
 home/dns_rewrites.go | 107 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 108 insertions(+)
 create mode 100644 home/dns_rewrites.go

diff --git a/home/control.go b/home/control.go
index b012f9e9..daaf5dbd 100644
--- a/home/control.go
+++ b/home/control.go
@@ -1021,6 +1021,7 @@ func registerControlHandlers() {
 
 	RegisterTLSHandlers()
 	RegisterClientsHandlers()
+	registerRewritesHandlers()
 
 	http.HandleFunc("/dns-query", postInstall(handleDOH))
 }
diff --git a/home/dns_rewrites.go b/home/dns_rewrites.go
new file mode 100644
index 00000000..816739ab
--- /dev/null
+++ b/home/dns_rewrites.go
@@ -0,0 +1,107 @@
+package home
+
+import (
+	"encoding/json"
+	"net/http"
+
+	"github.com/AdguardTeam/AdGuardHome/dnsfilter"
+	"github.com/AdguardTeam/golibs/log"
+)
+
+type rewriteEntryJSON struct {
+	Domain string `json:"domain"`
+	Answer string `json:"answer"`
+}
+
+func handleRewriteList(w http.ResponseWriter, r *http.Request) {
+	log.Tracef("%s %v", r.Method, r.URL)
+
+	arr := []*rewriteEntryJSON{}
+
+	config.RLock()
+	for _, ent := range config.DNS.Rewrites {
+		jsent := rewriteEntryJSON{
+			Domain: ent.Domain,
+			Answer: ent.Answer,
+		}
+		arr = append(arr, &jsent)
+	}
+	config.RUnlock()
+
+	w.Header().Set("Content-Type", "application/json")
+	err := json.NewEncoder(w).Encode(arr)
+	if err != nil {
+		httpError(w, http.StatusInternalServerError, "json.Encode: %s", err)
+		return
+	}
+}
+
+func handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
+	log.Tracef("%s %v", r.Method, r.URL)
+
+	jsent := rewriteEntryJSON{}
+	err := json.NewDecoder(r.Body).Decode(&jsent)
+	if err != nil {
+		httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
+		return
+	}
+
+	ent := dnsfilter.RewriteEntry{
+		Domain: jsent.Domain,
+		Answer: jsent.Answer,
+	}
+	config.Lock()
+	config.DNS.Rewrites = append(config.DNS.Rewrites, ent)
+	config.Unlock()
+	log.Debug("Rewrites: added element: %s -> %s [%d]",
+		ent.Domain, ent.Answer, len(config.DNS.Rewrites))
+
+	err = writeAllConfigsAndReloadDNS()
+	if err != nil {
+		httpError(w, http.StatusBadRequest, "%s", err)
+		return
+	}
+
+	returnOK(w)
+}
+
+func handleRewriteDelete(w http.ResponseWriter, r *http.Request) {
+	log.Tracef("%s %v", r.Method, r.URL)
+
+	jsent := rewriteEntryJSON{}
+	err := json.NewDecoder(r.Body).Decode(&jsent)
+	if err != nil {
+		httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
+		return
+	}
+
+	entDel := dnsfilter.RewriteEntry{
+		Domain: jsent.Domain,
+		Answer: jsent.Answer,
+	}
+	arr := []dnsfilter.RewriteEntry{}
+	config.Lock()
+	for _, ent := range config.DNS.Rewrites {
+		if ent == entDel {
+			log.Debug("Rewrites: removed element: %s -> %s", ent.Domain, ent.Answer)
+			continue
+		}
+		arr = append(arr, ent)
+	}
+	config.DNS.Rewrites = arr
+	config.Unlock()
+
+	err = writeAllConfigsAndReloadDNS()
+	if err != nil {
+		httpError(w, http.StatusBadRequest, "%s", err)
+		return
+	}
+
+	returnOK(w)
+}
+
+func registerRewritesHandlers() {
+	http.HandleFunc("/control/rewrite/list", postInstall(optionalAuth(ensureGET(handleRewriteList))))
+	http.HandleFunc("/control/rewrite/add", postInstall(optionalAuth(ensurePOST(handleRewriteAdd))))
+	http.HandleFunc("/control/rewrite/delete", postInstall(optionalAuth(ensurePOST(handleRewriteDelete))))
+}

From 1bb6638db796bd733a4b3a0066e7d045579119ec Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Mon, 29 Jul 2019 11:37:16 +0300
Subject: [PATCH 4/6] + dnsforward: use Rewrites table

---
 dnsfilter/dnsfilter.go   | 77 +++++++++++++++++++++++++++++++++++++++-
 dnsforward/dnsforward.go | 70 ++++++++++++++++++++++++++++++++++--
 2 files changed, 143 insertions(+), 4 deletions(-)

diff --git a/dnsfilter/dnsfilter.go b/dnsfilter/dnsfilter.go
index a642abe4..20d17493 100644
--- a/dnsfilter/dnsfilter.go
+++ b/dnsfilter/dnsfilter.go
@@ -39,7 +39,7 @@ const defaultParentalURL = "%s://%s/check-parental-control-hash?prefixes=%s&sens
 const defaultParentalSensitivity = 13 // use "TEEN" by default
 const maxDialCacheSize = 2            // the number of host names for safebrowsing and parental control
 
-// Custom filtering settings
+// RequestFilteringSettings is custom filtering settings
 type RequestFilteringSettings struct {
 	FilteringEnabled    bool
 	SafeSearchEnabled   bool
@@ -47,6 +47,12 @@ type RequestFilteringSettings struct {
 	ParentalEnabled     bool
 }
 
+// RewriteEntry is a rewrite array element
+type RewriteEntry struct {
+	Domain string `yaml:"domain"`
+	Answer string `yaml:"answer"` // IP address or canonical name
+}
+
 // Config allows you to configure DNS filtering with New() or just change variables directly.
 type Config struct {
 	ParentalSensitivity int    `yaml:"parental_sensitivity"` // must be either 3, 10, 13 or 17
@@ -60,6 +66,8 @@ type Config struct {
 	SafeSearchCacheSize   int `yaml:"safesearch_cache_size"`
 	ParentalCacheSize     int `yaml:"parental_cache_size"`
 
+	Rewrites []RewriteEntry `yaml:"rewrites"`
+
 	// Filtering callback function
 	FilterHandler func(clientAddr string, settings *RequestFilteringSettings) `yaml:"-"`
 }
@@ -131,6 +139,9 @@ const (
 	FilteredInvalid
 	// FilteredSafeSearch - the host was replaced with safesearch variant
 	FilteredSafeSearch
+
+	// ReasonRewrite - rewrite rule was applied
+	ReasonRewrite
 )
 
 func (i Reason) String() string {
@@ -138,11 +149,14 @@ func (i Reason) String() string {
 		"NotFilteredNotFound",
 		"NotFilteredWhiteList",
 		"NotFilteredError",
+
 		"FilteredBlackList",
 		"FilteredSafeBrowsing",
 		"FilteredParental",
 		"FilteredInvalid",
 		"FilteredSafeSearch",
+
+		"Rewrite",
 	}
 	if uint(i) >= uint(len(names)) {
 		return ""
@@ -167,6 +181,10 @@ type Result struct {
 	Rule       string `json:",omitempty"` // Original rule text
 	IP         net.IP `json:",omitempty"` // Not nil only in the case of a hosts file syntax
 	FilterID   int64  `json:",omitempty"` // Filter ID the rule belongs to
+
+	// for ReasonRewrite:
+	CanonName string   `json:",omitempty"` // CNAME value
+	IPList    []net.IP `json:",omitempty"` // list of IP addresses
 }
 
 // Matched can be used to see if any match at all was found, no matter filtered or not
@@ -197,6 +215,12 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res
 
 	var result Result
 	var err error
+
+	result = d.processRewrites(host, qtype)
+	if result.Reason == ReasonRewrite {
+		return result, nil
+	}
+
 	// try filter lists first
 	if setts.FilteringEnabled {
 		result, err = d.matchHost(host, qtype)
@@ -251,6 +275,57 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res
 	return Result{}, nil
 }
 
+// Process rewrites table
+// . Find CNAME for a domain name
+//  . if found, set domain name to canonical name
+// . Find A or AAAA record for a domain name
+//  . if found, return IP addresses
+func (d *Dnsfilter) processRewrites(host string, qtype uint16) Result {
+	var res Result
+
+	for _, r := range d.Rewrites {
+		if r.Domain != host {
+			continue
+		}
+
+		ip := net.ParseIP(r.Answer)
+		if ip == nil {
+			log.Debug("Rewrite: CNAME for %s is %s", host, r.Answer)
+			host = r.Answer
+			res.CanonName = r.Answer
+			res.Reason = ReasonRewrite
+			break
+		}
+	}
+
+	for _, r := range d.Rewrites {
+		if r.Domain != host {
+			continue
+		}
+
+		ip := net.ParseIP(r.Answer)
+		if ip == nil {
+			continue
+		}
+		ip4 := ip.To4()
+
+		if qtype == dns.TypeA && ip4 != nil {
+			res.IPList = append(res.IPList, ip4)
+			log.Debug("Rewrite: A for %s is %s", host, ip4)
+
+		} else if qtype == dns.TypeAAAA && ip4 == nil {
+			res.IPList = append(res.IPList, ip)
+			log.Debug("Rewrite: AAAA for %s is %s", host, ip)
+		}
+	}
+
+	if len(res.IPList) != 0 {
+		res.Reason = ReasonRewrite
+	}
+
+	return res
+}
+
 func setCacheResult(cache *fastcache.Cache, host string, res Result) {
 	var buf bytes.Buffer
 	enc := gob.NewEncoder(&buf)
diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go
index ef9fbc20..1661f68b 100644
--- a/dnsforward/dnsforward.go
+++ b/dnsforward/dnsforward.go
@@ -453,11 +453,31 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
 	}
 
 	if d.Res == nil {
+		answer := []dns.RR{}
+		originalQuestion := d.Req.Question[0]
+
+		if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 {
+			answer = append(answer, s.genCNAMEAnswer(d.Req, res.CanonName))
+			// resolve canonical name, not the original host name
+			d.Req.Question[0].Name = dns.Fqdn(res.CanonName)
+		}
+
 		// request was not filtered so let it be processed further
 		err = p.Resolve(d)
 		if err != nil {
 			return err
 		}
+
+		if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 {
+
+			d.Req.Question[0] = originalQuestion
+			d.Res.Question[0] = originalQuestion
+
+			if len(d.Res.Answer) != 0 {
+				answer = append(answer, d.Res.Answer...) // host -> IP
+				d.Res.Answer = answer
+			}
+		}
 	}
 
 	shouldLog := true
@@ -485,8 +505,10 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
 
 // filterDNSRequest applies the dnsFilter and sets d.Res if the request was filtered
 func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error) {
-	msg := d.Req
-	host := strings.TrimSuffix(msg.Question[0].Name, ".")
+	var res dnsfilter.Result
+	req := d.Req
+	host := strings.TrimSuffix(req.Question[0].Name, ".")
+	origHost := host
 
 	s.RLock()
 	protectionEnabled := s.conf.ProtectionEnabled
@@ -497,7 +519,10 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
 		return nil, nil
 	}
 
-	var res dnsfilter.Result
+	if host != origHost {
+		log.Debug("Rewrite: not supported: CNAME for %s is %s", origHost, host)
+	}
+
 	var err error
 
 	clientAddr := ""
@@ -508,9 +533,35 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
 	if err != nil {
 		// Return immediately if there's an error
 		return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
+
 	} else if res.IsFiltered {
 		// log.Tracef("Host %s is filtered, reason - '%s', matched rule: '%s'", host, res.Reason, res.Rule)
 		d.Res = s.genDNSFilterMessage(d, &res)
+
+	} else if res.Reason == dnsfilter.ReasonRewrite && len(res.IPList) != 0 {
+		resp := dns.Msg{}
+		resp.SetReply(req)
+
+		name := host
+		if len(res.CanonName) != 0 {
+			resp.Answer = append(resp.Answer, s.genCNAMEAnswer(req, res.CanonName))
+			name = res.CanonName
+		}
+
+		for _, ip := range res.IPList {
+			if req.Question[0].Qtype == dns.TypeA {
+				a := s.genAAnswer(req, ip)
+				a.Hdr.Name = dns.Fqdn(name)
+				resp.Answer = append(resp.Answer, a)
+
+			} else if req.Question[0].Qtype == dns.TypeAAAA {
+				a := s.genAAAAAnswer(req, res.IP)
+				a.Hdr.Name = dns.Fqdn(name)
+				resp.Answer = append(resp.Answer, a)
+			}
+		}
+
+		d.Res = &resp
 	}
 
 	return &res, err
@@ -644,6 +695,19 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo
 	return &resp
 }
 
+// Make a CNAME response
+func (s *Server) genCNAMEAnswer(req *dns.Msg, cname string) *dns.CNAME {
+	answer := new(dns.CNAME)
+	answer.Hdr = dns.RR_Header{
+		Name:   req.Question[0].Name,
+		Rrtype: dns.TypeCNAME,
+		Ttl:    s.conf.BlockedResponseTTL,
+		Class:  dns.ClassINET,
+	}
+	answer.Target = dns.Fqdn(cname)
+	return answer
+}
+
 func (s *Server) genNXDomain(request *dns.Msg) *dns.Msg {
 	resp := dns.Msg{}
 	resp.SetRcode(request, dns.RcodeNameError)

From 70b8cf6ec840a22a5be6fae58167912f70b988a1 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Fri, 26 Jul 2019 17:09:46 +0300
Subject: [PATCH 5/6] + openapi: add /rewrite/* methods

---
 openapi/openapi.yaml | 67 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 67 insertions(+)

diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index fd472e87..d5ea137d 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -786,6 +786,54 @@ paths:
                 200:
                     description: OK
 
+    # --------------------------------------------------
+    # Rewrite methods
+    # --------------------------------------------------
+
+    /rewrite/list:
+        get:
+            tags:
+                - rewrite
+            operationId: rewriteList
+            summary: 'Get list of Rewrite rules'
+            responses:
+                200:
+                    description: OK
+                    schema:
+                        $ref: "#/definitions/RewriteList"
+
+    /rewrite/add:
+        post:
+            tags:
+                - rewrite
+            operationId: rewriteAdd
+            summary: 'Add a new Rewrite rule'
+            parameters:
+                - in: "body"
+                  name: "body"
+                  required: true
+                  schema:
+                      $ref: "#/definitions/RewriteEntry"
+            responses:
+                200:
+                    description: OK
+
+    /rewrite/delete:
+        post:
+            tags:
+                - rewrite
+            operationId: rewriteDelete
+            summary: 'Remove a Rewrite rule'
+            parameters:
+                - in: "body"
+                  name: "body"
+                  required: true
+                  schema:
+                      $ref: "#/definitions/RewriteEntry"
+            responses:
+                200:
+                    description: OK
+
     # --------------------------------------------------
     # I18N methods
     # --------------------------------------------------
@@ -1571,6 +1619,25 @@ definitions:
         items:
             $ref: "#/definitions/ClientAuto"
         description: "Auto-Clients array"
+
+    RewriteList:
+        type: "array"
+        items:
+            $ref: "#/definitions/RewriteEntry"
+        description: "Rewrite rules array"
+    RewriteEntry:
+        type: "object"
+        description: "Rewrite rule"
+        properties:
+            domain:
+                type: "string"
+                description: "Domain name"
+                example: "example.org"
+            answer:
+                type: "string"
+                description: "value of A, AAAA or CNAME DNS record"
+                example: "127.0.0.1"
+
     CheckConfigRequest:
         type: "object"
         description: "Configuration to be checked"

From e95aae5744ffd1de0809c95a8df50ea3f5f1af28 Mon Sep 17 00:00:00 2001
From: Ildar Kamalov <i.kamalov@adguard.com>
Date: Mon, 22 Jul 2019 15:32:12 +0300
Subject: [PATCH 6/6] + client: handle DNS rewrites

---
 client/package-lock.json                      | 86 +++++++++---------
 client/package.json                           |  5 +-
 client/src/__locales/en.json                  | 17 +++-
 client/src/actions/rewrites.js                | 58 ++++++++++++
 client/src/api/Api.js                         | 28 ++++++
 client/src/components/Logs/Logs.css           |  6 ++
 client/src/components/Logs/index.js           | 28 +++++-
 .../components/Settings/Dns/Rewrites/Form.js  | 89 +++++++++++++++++++
 .../components/Settings/Dns/Rewrites/Modal.js | 52 +++++++++++
 .../components/Settings/Dns/Rewrites/Table.js | 87 ++++++++++++++++++
 .../components/Settings/Dns/Rewrites/index.js | 83 +++++++++++++++++
 client/src/components/Settings/Dns/index.js   | 19 ++++
 client/src/components/ui/Card.js              |  3 +-
 client/src/components/ui/ReactTable.css       |  4 +
 client/src/containers/Dns.js                  | 15 +++-
 client/src/helpers/constants.js               |  2 +
 client/src/helpers/form.js                    | 48 +++++++---
 client/src/reducers/index.js                  |  2 +
 client/src/reducers/rewrites.js               | 50 +++++++++++
 client/webpack.dev.js                         | 19 ++--
 20 files changed, 631 insertions(+), 70 deletions(-)
 create mode 100644 client/src/actions/rewrites.js
 create mode 100644 client/src/components/Settings/Dns/Rewrites/Form.js
 create mode 100644 client/src/components/Settings/Dns/Rewrites/Modal.js
 create mode 100644 client/src/components/Settings/Dns/Rewrites/Table.js
 create mode 100644 client/src/components/Settings/Dns/Rewrites/index.js
 create mode 100644 client/src/reducers/rewrites.js

diff --git a/client/package-lock.json b/client/package-lock.json
index 3edaa670..cedee41d 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -945,12 +945,27 @@
       }
     },
     "axios": {
-      "version": "0.18.0",
-      "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
-      "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
+      "version": "0.18.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
+      "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
       "requires": {
-        "follow-redirects": "^1.3.0",
-        "is-buffer": "^1.1.5"
+        "follow-redirects": "1.5.10",
+        "is-buffer": "^2.0.2"
+      },
+      "dependencies": {
+        "follow-redirects": {
+          "version": "1.5.10",
+          "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+          "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+          "requires": {
+            "debug": "=3.1.0"
+          }
+        },
+        "is-buffer": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
+          "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
+        }
       }
     },
     "axobject-query": {
@@ -5124,6 +5139,7 @@
       "version": "1.5.7",
       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.7.tgz",
       "integrity": "sha512-NONJVIFiX7Z8k2WxfqBjtwqMifx7X42ORLFrOZ2LTKGj71G3C0kfdyTqGqr8fx5zSX6Foo/D95dgGWbPUiwnew==",
+      "dev": true,
       "requires": {
         "debug": "^3.1.0"
       }
@@ -6933,7 +6949,8 @@
     "is-buffer": {
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
-      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+      "dev": true
     },
     "is-builtin-module": {
       "version": "1.0.0",
@@ -7386,9 +7403,9 @@
       }
     },
     "lodash": {
-      "version": "4.17.11",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
     },
     "lodash-es": {
       "version": "4.17.10",
@@ -7767,9 +7784,9 @@
       }
     },
     "mixin-deep": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
-      "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+      "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
       "dev": true,
       "requires": {
         "for-in": "^1.0.2",
@@ -10191,6 +10208,14 @@
         }
       }
     },
+    "react-router-hash-link": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-1.2.2.tgz",
+      "integrity": "sha512-LBthLVHdqPeKDVt3+cFRhy15Z7veikOvdKRZRfyBR2vjqIE7rxn+tKLjb6DOmLm6JpoQVemVDnxQ35RVnEHdQA==",
+      "requires": {
+        "prop-types": "^15.6.0"
+      }
+    },
     "react-table": {
       "version": "6.8.6",
       "resolved": "https://registry.npmjs.org/react-table/-/react-table-6.8.6.tgz",
@@ -10848,9 +10873,9 @@
       "dev": true
     },
     "set-value": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
-      "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+      "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
       "dev": true,
       "requires": {
         "extend-shallow": "^2.0.1",
@@ -12478,38 +12503,15 @@
       }
     },
     "union-value": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
-      "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+      "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
       "dev": true,
       "requires": {
         "arr-union": "^3.1.0",
         "get-value": "^2.0.6",
         "is-extendable": "^0.1.1",
-        "set-value": "^0.4.3"
-      },
-      "dependencies": {
-        "extend-shallow": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-          "dev": true,
-          "requires": {
-            "is-extendable": "^0.1.0"
-          }
-        },
-        "set-value": {
-          "version": "0.4.3",
-          "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
-          "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
-          "dev": true,
-          "requires": {
-            "extend-shallow": "^2.0.1",
-            "is-extendable": "^0.1.1",
-            "is-plain-object": "^2.0.1",
-            "to-object-path": "^0.3.0"
-          }
-        }
+        "set-value": "^2.0.1"
       }
     },
     "uniq": {
diff --git a/client/package.json b/client/package.json
index 05d91b7a..dc64d590 100644
--- a/client/package.json
+++ b/client/package.json
@@ -10,13 +10,13 @@
   },
   "dependencies": {
     "@nivo/line": "^0.49.1",
-    "axios": "^0.18.0",
+    "axios": "^0.18.1",
     "classnames": "^2.2.6",
     "date-fns": "^1.29.0",
     "file-saver": "^1.3.8",
     "i18next": "^12.0.0",
     "i18next-browser-languagedetector": "^2.2.3",
-    "lodash": "^4.17.11",
+    "lodash": "^4.17.15",
     "nanoid": "^1.2.3",
     "prop-types": "^15.6.1",
     "react": "^16.4.0",
@@ -27,6 +27,7 @@
     "react-redux": "^5.0.7",
     "react-redux-loading-bar": "^4.0.7",
     "react-router-dom": "^4.2.2",
+    "react-router-hash-link": "^1.2.2",
     "react-table": "^6.8.6",
     "react-transition-group": "^2.4.0",
     "redux": "^4.0.0",
diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 78f8e94f..8fc269ca 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -331,5 +331,18 @@
     "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supports <1>DNS-over-HTTPS</1>.",
     "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supports <1>DNS-over-HTTPS</1>.",
     "setup_dns_privacy_other_5": "You will find more implementations <0>here</0> and <1>here</1>.",
-    "setup_dns_notice": "In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> in AdGuard Home settings."
-}
\ No newline at end of file
+    "setup_dns_notice": "In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> in AdGuard Home settings.",
+    "rewrite_added": "DNS rewrite for \"{{key}}\" successfully added",
+    "rewrite_deleted": "DNS rewrite for \"{{key}}\" successfully deleted",
+    "rewrite_add": "Add DNS rewrite",
+    "rewrite_not_found": "No DNS rewrites found",
+    "rewrite_confirm_delete": "Are you sure you want to delete DNS rewrite for \"{{key}}\"?",
+    "rewrite_desc": "Allows to easily configure custom DNS response for a specific domain name.",
+    "rewrite_applied": "Applied Rewrite rule",
+    "dns_rewrites": "DNS rewrites",
+    "form_domain": "Enter domain",
+    "form_answer": "Enter IP address or domain name",
+    "form_error_domain_format": "Invalid domain format",
+    "form_error_answer_format": "Invalid answer format",
+    "configure": "Configure"
+}
diff --git a/client/src/actions/rewrites.js b/client/src/actions/rewrites.js
new file mode 100644
index 00000000..df846fdd
--- /dev/null
+++ b/client/src/actions/rewrites.js
@@ -0,0 +1,58 @@
+import { createAction } from 'redux-actions';
+import { t } from 'i18next';
+import Api from '../api/Api';
+import { addErrorToast, addSuccessToast } from './index';
+
+const apiClient = new Api();
+
+export const toggleRewritesModal = createAction('TOGGLE_REWRITES_MODAL');
+
+export const getRewritesListRequest = createAction('GET_REWRITES_LIST_REQUEST');
+export const getRewritesListFailure = createAction('GET_REWRITES_LIST_FAILURE');
+export const getRewritesListSuccess = createAction('GET_REWRITES_LIST_SUCCESS');
+
+export const getRewritesList = () => async (dispatch) => {
+    dispatch(getRewritesListRequest());
+    try {
+        const data = await apiClient.getRewritesList();
+        dispatch(getRewritesListSuccess(data));
+    } catch (error) {
+        dispatch(addErrorToast({ error }));
+        dispatch(getRewritesListFailure());
+    }
+};
+
+export const addRewriteRequest = createAction('ADD_REWRITE_REQUEST');
+export const addRewriteFailure = createAction('ADD_REWRITE_FAILURE');
+export const addRewriteSuccess = createAction('ADD_REWRITE_SUCCESS');
+
+export const addRewrite = config => async (dispatch) => {
+    dispatch(addRewriteRequest());
+    try {
+        await apiClient.addRewrite(config);
+        dispatch(addRewriteSuccess(config));
+        dispatch(toggleRewritesModal());
+        dispatch(getRewritesList());
+        dispatch(addSuccessToast(t('rewrite_added', { key: config.domain })));
+    } catch (error) {
+        dispatch(addErrorToast({ error }));
+        dispatch(addRewriteFailure());
+    }
+};
+
+export const deleteRewriteRequest = createAction('DELETE_REWRITE_REQUEST');
+export const deleteRewriteFailure = createAction('DELETE_REWRITE_FAILURE');
+export const deleteRewriteSuccess = createAction('DELETE_REWRITE_SUCCESS');
+
+export const deleteRewrite = config => async (dispatch) => {
+    dispatch(deleteRewriteRequest());
+    try {
+        await apiClient.deleteRewrite(config);
+        dispatch(deleteRewriteSuccess());
+        dispatch(getRewritesList());
+        dispatch(addSuccessToast(t('rewrite_deleted', { key: config.domain })));
+    } catch (error) {
+        dispatch(addErrorToast({ error }));
+        dispatch(deleteRewriteFailure());
+    }
+};
diff --git a/client/src/api/Api.js b/client/src/api/Api.js
index 76b17888..766cd499 100644
--- a/client/src/api/Api.js
+++ b/client/src/api/Api.js
@@ -481,4 +481,32 @@ export default class Api {
         };
         return this.makeRequest(path, method, parameters);
     }
+
+    // DNS rewrites
+    REWRITES_LIST = { path: 'rewrite/list', method: 'GET' };
+    REWRITE_ADD = { path: 'rewrite/add', method: 'POST' };
+    REWRITE_DELETE = { path: 'rewrite/delete', method: 'POST' };
+
+    getRewritesList() {
+        const { path, method } = this.REWRITES_LIST;
+        return this.makeRequest(path, method);
+    }
+
+    addRewrite(config) {
+        const { path, method } = this.REWRITE_ADD;
+        const parameters = {
+            data: config,
+            headers: { 'Content-Type': 'application/json' },
+        };
+        return this.makeRequest(path, method, parameters);
+    }
+
+    deleteRewrite(config) {
+        const { path, method } = this.REWRITE_DELETE;
+        const parameters = {
+            data: config,
+            headers: { 'Content-Type': 'application/json' },
+        };
+        return this.makeRequest(path, method, parameters);
+    }
 }
diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css
index 1b39f592..3205e424 100644
--- a/client/src/components/Logs/Logs.css
+++ b/client/src/components/Logs/Logs.css
@@ -13,6 +13,12 @@
     overflow: hidden;
 }
 
+.logs__row--column {
+    flex-direction: column;
+    align-items: flex-start;
+    overflow: hidden;
+}
+
 .logs__row .list-unstyled {
     margin-bottom: 0;
     overflow: hidden;
diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js
index 8a675a05..f8891206 100644
--- a/client/src/components/Logs/index.js
+++ b/client/src/components/Logs/index.js
@@ -5,6 +5,7 @@ import { saveAs } from 'file-saver/FileSaver';
 import escapeRegExp from 'lodash/escapeRegExp';
 import endsWith from 'lodash/endsWith';
 import { Trans, withNamespaces } from 'react-i18next';
+import { HashLink as Link } from 'react-router-hash-link';
 
 import { formatTime, getClientName } from '../../helpers/helpers';
 import { getTrackerData } from '../../helpers/trackers/trackers';
@@ -125,6 +126,7 @@ class Logs extends Component {
                 const rule = row && row.original && row.original.rule;
                 const { filterId } = row.original;
                 const { filters } = this.props.filtering;
+                const isRewrite = reason && reason === 'Rewrite';
                 let filterName = '';
 
                 if (reason === 'FilteredBlackList' || reason === 'NotFilteredWhiteList') {
@@ -161,14 +163,16 @@ class Logs extends Component {
                     const isRenderTooltip = reason === 'NotFilteredWhiteList';
 
                     return (
-                        <div className="logs__row">
+                        <div className={`logs__row ${isRewrite && 'logs__row--column'}`}>
+                            {isRewrite && <strong><Trans>rewrite_applied</Trans></strong>}
                             <ul className="list-unstyled">{liNodes}</ul>
                             {this.renderTooltip(isRenderTooltip, rule, filterName)}
                         </div>
                     );
                 }
                 return (
-                    <div className="logs__row">
+                    <div className={`logs__row ${isRewrite && 'logs__row--column'}`}>
+                        {isRewrite && <strong><Trans>rewrite_applied</Trans></strong>}
                         <span><Trans>empty_response_status</Trans></span>
                         {this.renderTooltip(isFiltered, rule, filterName)}
                     </div>
@@ -197,6 +201,7 @@ class Logs extends Component {
             Cell: (row) => {
                 const { reason } = row.original;
                 const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
+                const isRewrite = reason && reason === 'Rewrite';
                 const clientName = getClientName(dashboard.clients, row.value)
                     || getClientName(dashboard.autoClients, row.value);
                 let client;
@@ -207,6 +212,21 @@ class Logs extends Component {
                     client = row.value;
                 }
 
+                if (isRewrite) {
+                    return (
+                        <Fragment>
+                            <div className="logs__row">
+                                {client}
+                            </div>
+                            <div className="logs__action">
+                                <Link to="/dns#rewrites" className="btn btn-sm btn-outline-primary">
+                                    <Trans>configure</Trans>
+                                </Link>
+                            </div>
+                        </Fragment>
+                    );
+                }
+
                 return (
                     <Fragment>
                         <div className="logs__row">
@@ -261,6 +281,10 @@ class Logs extends Component {
                         return {
                             className: 'green',
                         };
+                    } else if (rowInfo.original.reason === 'Rewrite') {
+                        return {
+                            className: 'blue',
+                        };
                     }
 
                     return {
diff --git a/client/src/components/Settings/Dns/Rewrites/Form.js b/client/src/components/Settings/Dns/Rewrites/Form.js
new file mode 100644
index 00000000..2d6b27a3
--- /dev/null
+++ b/client/src/components/Settings/Dns/Rewrites/Form.js
@@ -0,0 +1,89 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Field, reduxForm } from 'redux-form';
+import { Trans, withNamespaces } from 'react-i18next';
+import flow from 'lodash/flow';
+
+import { renderField, required, domain, answer } from '../../../../helpers/form';
+
+const Form = (props) => {
+    const {
+        t,
+        handleSubmit,
+        reset,
+        pristine,
+        submitting,
+        toggleRewritesModal,
+        processingAdd,
+    } = props;
+
+    return (
+        <form onSubmit={handleSubmit}>
+            <div className="modal-body">
+                <div className="form__group">
+                    <Field
+                        id="domain"
+                        name="domain"
+                        component={renderField}
+                        type="text"
+                        className="form-control"
+                        placeholder={t('form_domain')}
+                        validate={[required, domain]}
+                    />
+                </div>
+                <div className="form__group">
+                    <Field
+                        id="answer"
+                        name="answer"
+                        component={renderField}
+                        type="text"
+                        className="form-control"
+                        placeholder={t('form_answer')}
+                        validate={[required, answer]}
+                    />
+                </div>
+            </div>
+
+            <div className="modal-footer">
+                <div className="btn-list">
+                    <button
+                        type="button"
+                        className="btn btn-secondary btn-standard"
+                        disabled={submitting || processingAdd}
+                        onClick={() => {
+                            reset();
+                            toggleRewritesModal();
+                        }}
+                    >
+                        <Trans>cancel_btn</Trans>
+                    </button>
+                    <button
+                        type="submit"
+                        className="btn btn-success btn-standard"
+                        disabled={submitting || pristine || processingAdd}
+                    >
+                        <Trans>save_btn</Trans>
+                    </button>
+                </div>
+            </div>
+        </form>
+    );
+};
+
+Form.propTypes = {
+    pristine: PropTypes.bool.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    reset: PropTypes.func.isRequired,
+    toggleRewritesModal: PropTypes.func.isRequired,
+    submitting: PropTypes.bool.isRequired,
+    processingAdd: PropTypes.bool.isRequired,
+    t: PropTypes.func.isRequired,
+};
+
+export default flow([
+    withNamespaces(),
+    reduxForm({
+        form: 'rewritesForm',
+        enableReinitialize: true,
+    }),
+])(Form);
diff --git a/client/src/components/Settings/Dns/Rewrites/Modal.js b/client/src/components/Settings/Dns/Rewrites/Modal.js
new file mode 100644
index 00000000..eba58bb9
--- /dev/null
+++ b/client/src/components/Settings/Dns/Rewrites/Modal.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Trans, withNamespaces } from 'react-i18next';
+import ReactModal from 'react-modal';
+
+import Form from './Form';
+
+const Modal = (props) => {
+    const {
+        isModalOpen,
+        handleSubmit,
+        toggleRewritesModal,
+        processingAdd,
+        processingDelete,
+    } = props;
+
+    return (
+        <ReactModal
+            className="Modal__Bootstrap modal-dialog modal-dialog-centered"
+            closeTimeoutMS={0}
+            isOpen={isModalOpen}
+            onRequestClose={() => toggleRewritesModal()}
+        >
+            <div className="modal-content">
+                <div className="modal-header">
+                    <h4 className="modal-title">
+                        <Trans>Add DNS rewrite</Trans>
+                    </h4>
+                    <button type="button" className="close" onClick={() => toggleRewritesModal()}>
+                        <span className="sr-only">Close</span>
+                    </button>
+                </div>
+                <Form
+                    onSubmit={handleSubmit}
+                    toggleRewritesModal={toggleRewritesModal}
+                    processingAdd={processingAdd}
+                    processingDelete={processingDelete}
+                />
+            </div>
+        </ReactModal>
+    );
+};
+
+Modal.propTypes = {
+    isModalOpen: PropTypes.bool.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    toggleRewritesModal: PropTypes.func.isRequired,
+    processingAdd: PropTypes.bool.isRequired,
+    processingDelete: PropTypes.bool.isRequired,
+};
+
+export default withNamespaces()(Modal);
diff --git a/client/src/components/Settings/Dns/Rewrites/Table.js b/client/src/components/Settings/Dns/Rewrites/Table.js
new file mode 100644
index 00000000..78d5489c
--- /dev/null
+++ b/client/src/components/Settings/Dns/Rewrites/Table.js
@@ -0,0 +1,87 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import ReactTable from 'react-table';
+import { withNamespaces } from 'react-i18next';
+
+class Table extends Component {
+    cellWrap = ({ value }) => (
+        <div className="logs__row logs__row--overflow">
+            <span className="logs__text" title={value}>
+                {value}
+            </span>
+        </div>
+    );
+
+    columns = [
+        {
+            Header: 'Domain',
+            accessor: 'domain',
+            Cell: this.cellWrap,
+        },
+        {
+            Header: 'Answer',
+            accessor: 'answer',
+            Cell: this.cellWrap,
+        },
+        {
+            Header: this.props.t('actions_table_header'),
+            accessor: 'actions',
+            maxWidth: 100,
+            Cell: value => (
+                <div className="logs__row logs__row--center">
+                    <button
+                        type="button"
+                        className="btn btn-icon btn-outline-secondary btn-sm"
+                        onClick={() =>
+                            this.props.handleDelete({
+                                answer: value.row.answer,
+                                domain: value.row.domain,
+                            })
+                        }
+                        title={this.props.t('delete_table_action')}
+                    >
+                        <svg className="icons">
+                            <use xlinkHref="#delete" />
+                        </svg>
+                    </button>
+                </div>
+            ),
+        },
+    ];
+
+    render() {
+        const {
+            t, list, processing, processingAdd, processingDelete,
+        } = this.props;
+
+        return (
+            <ReactTable
+                data={list || []}
+                columns={this.columns}
+                loading={processing || processingAdd || processingDelete}
+                className="-striped -highlight card-table-overflow"
+                showPagination={true}
+                defaultPageSize={10}
+                minRows={5}
+                previousText={t('previous_btn')}
+                nextText={t('next_btn')}
+                loadingText={t('loading_table_status')}
+                pageText={t('page_table_footer_text')}
+                ofText={t('of_table_footer_text')}
+                rowsText={t('rows_table_footer_text')}
+                noDataText={t('rewrite_not_found')}
+            />
+        );
+    }
+}
+
+Table.propTypes = {
+    t: PropTypes.func.isRequired,
+    list: PropTypes.array.isRequired,
+    processing: PropTypes.bool.isRequired,
+    processingAdd: PropTypes.bool.isRequired,
+    processingDelete: PropTypes.bool.isRequired,
+    handleDelete: PropTypes.func.isRequired,
+};
+
+export default withNamespaces()(Table);
diff --git a/client/src/components/Settings/Dns/Rewrites/index.js b/client/src/components/Settings/Dns/Rewrites/index.js
new file mode 100644
index 00000000..e4e0b193
--- /dev/null
+++ b/client/src/components/Settings/Dns/Rewrites/index.js
@@ -0,0 +1,83 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { Trans, withNamespaces } from 'react-i18next';
+
+import Table from './Table';
+import Modal from './Modal';
+import Card from '../../../ui/Card';
+
+class Rewrites extends Component {
+    handleSubmit = (values) => {
+        this.props.addRewrite(values);
+    };
+
+    handleDelete = (values) => {
+        // eslint-disable-next-line no-alert
+        if (window.confirm(this.props.t('rewrite_confirm_delete', { key: values.domain }))) {
+            this.props.deleteRewrite(values);
+        }
+    };
+
+    render() {
+        const {
+            t,
+            rewrites,
+            toggleRewritesModal,
+        } = this.props;
+
+        const {
+            list,
+            isModalOpen,
+            processing,
+            processingAdd,
+            processingDelete,
+        } = rewrites;
+
+        return (
+            <Card
+                id="rewrites"
+                title={t('dns_rewrites')}
+                subtitle={t('rewrite_desc')}
+                bodyType="card-body box-body--settings"
+            >
+                <Fragment>
+                    <Table
+                        list={list}
+                        processing={processing}
+                        processingAdd={processingAdd}
+                        processingDelete={processingDelete}
+                        handleDelete={this.handleDelete}
+                    />
+
+                    <button
+                        type="button"
+                        className="btn btn-success btn-standard mt-3"
+                        onClick={() => toggleRewritesModal()}
+                        disabled={processingAdd}
+                    >
+                        <Trans>rewrite_add</Trans>
+                    </button>
+
+                    <Modal
+                        isModalOpen={isModalOpen}
+                        toggleRewritesModal={toggleRewritesModal}
+                        handleSubmit={this.handleSubmit}
+                        processingAdd={processingAdd}
+                        processingDelete={processingDelete}
+                    />
+                </Fragment>
+            </Card>
+        );
+    }
+}
+
+Rewrites.propTypes = {
+    t: PropTypes.func.isRequired,
+    getRewritesList: PropTypes.func.isRequired,
+    toggleRewritesModal: PropTypes.func.isRequired,
+    addRewrite: PropTypes.func.isRequired,
+    deleteRewrite: PropTypes.func.isRequired,
+    rewrites: PropTypes.object.isRequired,
+};
+
+export default withNamespaces()(Rewrites);
diff --git a/client/src/components/Settings/Dns/index.js b/client/src/components/Settings/Dns/index.js
index 2b038b1f..cb9c9e4a 100644
--- a/client/src/components/Settings/Dns/index.js
+++ b/client/src/components/Settings/Dns/index.js
@@ -4,12 +4,14 @@ import { withNamespaces } from 'react-i18next';
 
 import Upstream from './Upstream';
 import Access from './Access';
+import Rewrites from './Rewrites';
 import PageTitle from '../../ui/PageTitle';
 import Loading from '../../ui/Loading';
 
 class Dns extends Component {
     componentDidMount() {
         this.props.getAccessList();
+        this.props.getRewritesList();
     }
 
     render() {
@@ -18,9 +20,14 @@ class Dns extends Component {
             dashboard,
             settings,
             access,
+            rewrites,
             setAccessList,
             testUpstream,
             setUpstream,
+            getRewritesList,
+            addRewrite,
+            deleteRewrite,
+            toggleRewritesModal,
         } = this.props;
 
         return (
@@ -39,6 +46,13 @@ class Dns extends Component {
                             testUpstream={testUpstream}
                         />
                         <Access access={access} setAccessList={setAccessList} />
+                        <Rewrites
+                            rewrites={rewrites}
+                            getRewritesList={getRewritesList}
+                            addRewrite={addRewrite}
+                            deleteRewrite={deleteRewrite}
+                            toggleRewritesModal={toggleRewritesModal}
+                        />
                     </Fragment>
                 )}
             </Fragment>
@@ -54,6 +68,11 @@ Dns.propTypes = {
     getAccessList: PropTypes.func.isRequired,
     setAccessList: PropTypes.func.isRequired,
     access: PropTypes.object.isRequired,
+    rewrites: PropTypes.object.isRequired,
+    getRewritesList: PropTypes.func.isRequired,
+    addRewrite: PropTypes.func.isRequired,
+    deleteRewrite: PropTypes.func.isRequired,
+    toggleRewritesModal: PropTypes.func.isRequired,
     t: PropTypes.func.isRequired,
 };
 
diff --git a/client/src/components/ui/Card.js b/client/src/components/ui/Card.js
index 01a06dac..65c15487 100644
--- a/client/src/components/ui/Card.js
+++ b/client/src/components/ui/Card.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import './Card.css';
 
 const Card = props => (
-    <div className={props.type ? `card ${props.type}` : 'card'}>
+    <div className={props.type ? `card ${props.type}` : 'card'} id={props.id ? props.id : ''}>
         {props.title &&
         <div className="card-header with-border">
             <div className="card-inner">
@@ -30,6 +30,7 @@ const Card = props => (
 );
 
 Card.propTypes = {
+    id: PropTypes.string,
     title: PropTypes.string,
     subtitle: PropTypes.string,
     bodyType: PropTypes.string,
diff --git a/client/src/components/ui/ReactTable.css b/client/src/components/ui/ReactTable.css
index e1de27b4..48a35dc7 100644
--- a/client/src/components/ui/ReactTable.css
+++ b/client/src/components/ui/ReactTable.css
@@ -15,3 +15,7 @@
 .rt-tr-group .green {
     background-color: #f1faf3;
 }
+
+.rt-tr-group .blue {
+    background-color: #ecf7ff;
+}
diff --git a/client/src/containers/Dns.js b/client/src/containers/Dns.js
index aa3c78ad..4f245c1a 100644
--- a/client/src/containers/Dns.js
+++ b/client/src/containers/Dns.js
@@ -1,14 +1,23 @@
 import { connect } from 'react-redux';
 import { handleUpstreamChange, setUpstream, testUpstream } from '../actions';
 import { getAccessList, setAccessList } from '../actions/access';
+import {
+    getRewritesList,
+    addRewrite,
+    deleteRewrite,
+    toggleRewritesModal,
+} from '../actions/rewrites';
 import Dns from '../components/Settings/Dns';
 
 const mapStateToProps = (state) => {
-    const { dashboard, settings, access } = state;
+    const {
+        dashboard, settings, access, rewrites,
+    } = state;
     const props = {
         dashboard,
         settings,
         access,
+        rewrites,
     };
     return props;
 };
@@ -19,6 +28,10 @@ const mapDispatchToProps = {
     testUpstream,
     getAccessList,
     setAccessList,
+    getRewritesList,
+    addRewrite,
+    deleteRewrite,
+    toggleRewritesModal,
 };
 
 export default connect(
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index 9faf2e5f..896d873e 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -1,5 +1,7 @@
 export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/[^/\s]+(\/.*)?$/;
+export const R_HOST = /^(([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])$/;
 export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g;
+export const R_IPV6 = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/g;
 export const R_MAC = /^((([a-fA-F0-9][a-fA-F0-9]+[-]){5}|([a-fA-F0-9][a-fA-F0-9]+[:]){5})([a-fA-F0-9][a-fA-F0-9])$)|(^([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]+[.]){2}([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]))$/g;
 
 export const STATS_NAMES = {
diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js
index 39c4b7aa..c4ffcb73 100644
--- a/client/src/helpers/form.js
+++ b/client/src/helpers/form.js
@@ -1,10 +1,16 @@
 import React, { Fragment } from 'react';
 import { Trans } from 'react-i18next';
 
-import { R_IPV4, R_MAC, UNSAFE_PORTS } from '../helpers/constants';
+import { R_IPV4, R_MAC, R_HOST, R_IPV6, UNSAFE_PORTS } from '../helpers/constants';
 
 export const renderField = ({
-    input, id, className, placeholder, type, disabled, meta: { touched, error },
+    input,
+    id,
+    className,
+    placeholder,
+    type,
+    disabled,
+    meta: { touched, error },
 }) => (
     <Fragment>
         <input
@@ -15,7 +21,9 @@ export const renderField = ({
             className={className}
             disabled={disabled}
         />
-        {!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
+        {!disabled &&
+            touched &&
+            (error && <span className="form__message form__message--error">{error}</span>)}
     </Fragment>
 );
 
@@ -24,20 +32,17 @@ export const renderSelectField = ({
 }) => (
     <Fragment>
         <label className="checkbox checkbox--form">
-            <span className="checkbox__marker"/>
-            <input
-                {...input}
-                type="checkbox"
-                className="checkbox__input"
-                disabled={disabled}
-            />
+            <span className="checkbox__marker" />
+            <input {...input} type="checkbox" className="checkbox__input" disabled={disabled} />
             <span className="checkbox__label">
                 <span className="checkbox__label-text checkbox__label-text--long">
                     <span className="checkbox__label-title">{placeholder}</span>
                 </span>
             </span>
         </label>
-        {!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
+        {!disabled &&
+            touched &&
+            (error && <span className="form__message form__message--error">{error}</span>)}
     </Fragment>
 );
 
@@ -63,7 +68,7 @@ export const mac = (value) => {
 };
 
 export const isPositive = (value) => {
-    if ((value || value === 0) && (value <= 0)) {
+    if ((value || value === 0) && value <= 0) {
         return <Trans>form_error_positive</Trans>;
     }
     return false;
@@ -92,4 +97,23 @@ export const isSafePort = (value) => {
     return false;
 };
 
+export const domain = (value) => {
+    if (value && !new RegExp(R_HOST).test(value)) {
+        return <Trans>form_error_domain_format</Trans>;
+    }
+    return false;
+};
+
+export const answer = (value) => {
+    if (
+        value &&
+        (!new RegExp(R_IPV4).test(value) &&
+            !new RegExp(R_IPV6).test(value) &&
+            !new RegExp(R_HOST).test(value))
+    ) {
+        return <Trans>form_error_answer_format</Trans>;
+    }
+    return false;
+};
+
 export const toNumber = value => value && parseInt(value, 10);
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index d3eb7342..32026a08 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -9,6 +9,7 @@ import toasts from './toasts';
 import encryption from './encryption';
 import clients from './clients';
 import access from './access';
+import rewrites from './rewrites';
 
 const settings = handleActions({
     [actions.initSettingsRequest]: state => ({ ...state, processing: true }),
@@ -422,6 +423,7 @@ export default combineReducers({
     encryption,
     clients,
     access,
+    rewrites,
     loadingBar: loadingBarReducer,
     form: formReducer,
 });
diff --git a/client/src/reducers/rewrites.js b/client/src/reducers/rewrites.js
new file mode 100644
index 00000000..d680b830
--- /dev/null
+++ b/client/src/reducers/rewrites.js
@@ -0,0 +1,50 @@
+import { handleActions } from 'redux-actions';
+
+import * as actions from '../actions/rewrites';
+
+const rewrites = handleActions(
+    {
+        [actions.getRewritesListRequest]: state => ({ ...state, processing: true }),
+        [actions.getRewritesListFailure]: state => ({ ...state, processing: false }),
+        [actions.getRewritesListSuccess]: (state, { payload }) => {
+            const newState = {
+                ...state,
+                list: payload,
+                processing: false,
+            };
+            return newState;
+        },
+
+        [actions.addRewriteRequest]: state => ({ ...state, processingAdd: true }),
+        [actions.addRewriteFailure]: state => ({ ...state, processingAdd: false }),
+        [actions.addRewriteSuccess]: (state, { payload }) => {
+            const newState = {
+                ...state,
+                list: [...state.list, ...payload],
+                processingAdd: false,
+            };
+            return newState;
+        },
+
+        [actions.deleteRewriteRequest]: state => ({ ...state, processingDelete: true }),
+        [actions.deleteRewriteFailure]: state => ({ ...state, processingDelete: false }),
+        [actions.deleteRewriteSuccess]: state => ({ ...state, processingDelete: false }),
+
+        [actions.toggleRewritesModal]: (state) => {
+            const newState = {
+                ...state,
+                isModalOpen: !state.isModalOpen,
+            };
+            return newState;
+        },
+    },
+    {
+        processing: true,
+        processingAdd: false,
+        processingDelete: false,
+        isModalOpen: false,
+        list: [],
+    },
+);
+
+export default rewrites;
diff --git a/client/webpack.dev.js b/client/webpack.dev.js
index 79589a08..442921e4 100644
--- a/client/webpack.dev.js
+++ b/client/webpack.dev.js
@@ -2,15 +2,18 @@ const merge = require('webpack-merge');
 const common = require('./webpack.common.js');
 
 module.exports = merge(common, {
+    devtool: 'eval-source-map',
     module: {
-        rules: [{
-            test: /\.js$/,
-            exclude: /node_modules/,
-            loader: 'eslint-loader',
-            options: {
-                emitWarning: true,
-                configFile: 'dev.eslintrc',
+        rules: [
+            {
+                test: /\.js$/,
+                exclude: /node_modules/,
+                loader: 'eslint-loader',
+                options: {
+                    emitWarning: true,
+                    configFile: 'dev.eslintrc',
+                },
             },
-        }],
+        ],
     },
 });