Compare commits

...

5 Commits

Author SHA1 Message Date
Dimitry Kolyshev
df209c99bd dnsforward: allowed clients private nets 2023-05-18 14:11:01 +03:00
Dimitry Kolyshev
668155b367 dnsforward: allowed clients private nets 2023-05-18 13:59:37 +03:00
Dimitry Kolyshev
9caf0d54c6 dnsforward: allowed clients local 2023-05-18 11:32:35 +03:00
Stanislav Chzhen
b72a3d01b8 Pull request 1845: AG-21324-update-dhcp
Merge in DNS/adguard-home from AG-21324-update-dhcp to master

Squashed commit of the following:

commit 20499d71ffe62f34576f9328db5dc6fb5c929c28
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed May 17 14:23:09 2023 +0300

    dhcpd: imp tests

commit fa6add1410a98d1b9dfd833bcb20ef9fb7ff57ca
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue May 16 09:52:38 2023 +0100

    all: update dhcp
2023-05-17 15:16:02 +03:00
Dimitry Kolyshev
0393e41096 Pull request: 1577: rewrite edit http api
Merge in DNS/adguard-home from 1577-rewrite-edit to master

Squashed commit of the following:

commit d03bee2a14337d169eea950b3df18a447c02b422
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri May 12 12:54:15 2023 +0300

    filtering: imp tests

commit bd68320df6dc057d922d91551cd00c74ebfaad6c
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri May 12 10:38:12 2023 +0300

    filtering: rewrite http tests

commit 0d8bbcd0194c0db89a6d4b45927669423c9bbb59
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu May 11 12:06:30 2023 +0300

    filtering: rewrite http tests

commit 29080384dd8fa80d5286d2fac1a4429d712bbafa
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 10 17:24:00 2023 +0300

    filtering: imp code

commit 96c6b1c98debfae565c5e6254746959a4307744e
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 10 16:56:32 2023 +0300

    filtering: imp code

commit b5d0c50ea11f9d829ba9d2b188fcc471a965e012
Merge: 5fa9e1c37 c77b2a0ce
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 10 16:37:19 2023 +0300

    Merge remote-tracking branch 'origin/master' into 1577-rewrite-edit

commit 5fa9e1c3714e107f893c03efa72227f3ed88691c
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 10 16:35:19 2023 +0300

    filtering: imp code

commit dd9dce8fbf0ce4bd200f2fc2fbf580e025920cd5
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 10 13:06:43 2023 +0300

    docs: rewrite http update

commit 0c67b040e80787b084c4669bb20db8d6d145fc1b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 10 13:04:31 2023 +0300

    filtering: rewrite http update
2023-05-12 13:04:19 +03:00
14 changed files with 466 additions and 57 deletions

View File

@@ -23,8 +23,16 @@ See also the [v0.107.30 GitHub milestone][ms-v0.107.30].
NOTE: Add new changes BELOW THIS COMMENT.
-->
### Added
- The ability to edit rewrite rules via `PUT /control/rewrite/update` HTTP API
([#1577]).
### Fixed
- Private networks are now ignored from allowed/blocked clients access lists.
These addresses are always allowed ([#5799]).
- Unquoted IPv6 bind hosts with trailing colons erroneously considered
unspecified addresses are now properly validated ([#5752]).
@@ -35,7 +43,9 @@ NOTE: Add new changes BELOW THIS COMMENT.
- Provided bootstrap servers are now used to resolve the hostnames of plain
UDP/TCP upstream servers.
[#1577]: https://github.com/AdguardTeam/AdGuardHome/issues/1577
[#5716]: https://github.com/AdguardTeam/AdGuardHome/issues/5716
[#5799]: https://github.com/AdguardTeam/AdGuardHome/issues/5799
<!--
NOTE: Add new changes ABOVE THIS COMMENT.

2
go.mod
View File

@@ -16,7 +16,7 @@ require (
github.com/google/gopacket v1.1.19
github.com/google/renameio v1.0.1
github.com/google/uuid v1.3.0
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8
github.com/insomniacslk/dhcp v0.0.0-20230516061539-49801966e6cb
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/kardianos/service v1.2.2
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118

2
go.sum
View File

@@ -67,6 +67,8 @@ github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg=
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE=
github.com/insomniacslk/dhcp v0.0.0-20230516061539-49801966e6cb h1:6fDKEAXwe3rsfS4khW3EZ8kEqmSiV9szhMPcDrD+Y7Q=
github.com/insomniacslk/dhcp v0.0.0-20230516061539-49801966e6cb/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=

View File

@@ -304,7 +304,7 @@ func tryConn6(req *dhcpv6.Message, c net.PacketConn) (ok, next bool, err error)
if !(response.Type() == dhcpv6.MessageTypeAdvertise &&
msg.TransactionID == req.TransactionID &&
rcid != nil &&
cid.Equal(*rcid)) {
cid.Equal(rcid)) {
log.Debug("dhcpv6: received message from server doesn't match our request")

View File

@@ -30,7 +30,7 @@ type v6Server struct {
leasesLock sync.Mutex
leases []*Lease
ipAddrs [256]byte
sid dhcpv6.Duid
sid dhcpv6.DUID
ra raCtx // RA module
@@ -659,9 +659,8 @@ func (s *v6Server) Start() (err error) {
return fmt.Errorf("validating interface %s: %w", iface.Name, err)
}
s.sid = dhcpv6.Duid{
Type: dhcpv6.DUID_LLT,
HwType: iana.HWTypeEthernet,
s.sid = &dhcpv6.DUIDLLT{
HWType: iana.HWTypeEthernet,
LinkLayerAddr: iface.HardwareAddr,
Time: dhcpv6.GetTime(),
}

View File

@@ -121,9 +121,8 @@ func TestV6GetLease(t *testing.T) {
dnsAddr := net.ParseIP("2000::1")
s.conf.dnsIPAddrs = []net.IP{dnsAddr}
s.sid = dhcpv6.Duid{
Type: dhcpv6.DUID_LLT,
HwType: iana.HWTypeEthernet,
s.sid = &dhcpv6.DUIDLL{
HWType: iana.HWTypeEthernet,
LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
}
@@ -216,9 +215,8 @@ func TestV6GetDynamicLease(t *testing.T) {
dnsAddr := net.ParseIP("2000::1")
s.conf.dnsIPAddrs = []net.IP{dnsAddr}
s.sid = dhcpv6.Duid{
Type: dhcpv6.DUID_LLT,
HwType: iana.HWTypeEthernet,
s.sid = &dhcpv6.DUIDLL{
HWType: iana.HWTypeEthernet,
LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/filterlist"
@@ -30,6 +31,9 @@ type accessManager struct {
blockedHostsEng *urlfilter.DNSEngine
// privateNets is the set of IP networks those are ignored by the manager.
privateNets netutil.SubnetSet
// TODO(a.garipov): Create a type for a set of IP networks.
allowedNets []netip.Prefix
blockedNets []netip.Prefix
@@ -64,13 +68,20 @@ func processAccessClients(
}
// newAccessCtx creates a new accessCtx.
func newAccessCtx(allowed, blocked, blockedHosts []string) (a *accessManager, err error) {
func newAccessCtx(
allowed []string,
blocked []string,
blockedHosts []string,
privateNets netutil.SubnetSet,
) (a *accessManager, err error) {
a = &accessManager{
allowedIPs: map[netip.Addr]unit{},
blockedIPs: map[netip.Addr]unit{},
allowedClientIDs: stringutil.NewSet(),
blockedClientIDs: stringutil.NewSet(),
privateNets: privateNets,
}
err = processAccessClients(allowed, a.allowedIPs, &a.allowedNets, a.allowedClientIDs)
@@ -138,9 +149,13 @@ func (a *accessManager) isBlockedHost(host string, qt rules.RRType) (ok bool) {
return ok
}
// isBlockedIP returns the status of the IP address blocking as well as the rule
// that blocked it.
// isBlockedIP returns the status of the IP address blocking as well as the
// rule that blocked it. Addresses from private nets are always allowed.
func (a *accessManager) isBlockedIP(ip netip.Addr) (blocked bool, rule string) {
if a.privateNets != nil && a.privateNets.Contains(ip.AsSlice()) {
return false, ""
}
blocked = true
ips := a.blockedIPs
ipnets := a.blockedNets
@@ -241,7 +256,7 @@ func (s *Server) handleAccessSet(w http.ResponseWriter, r *http.Request) {
}
var a *accessManager
a, err = newAccessCtx(list.AllowedClients, list.DisallowedClients, list.BlockedHosts)
a, err = newAccessCtx(list.AllowedClients, list.DisallowedClients, list.BlockedHosts, nil)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "creating access ctx: %s", err)

View File

@@ -4,6 +4,7 @@ import (
"net/netip"
"testing"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
@@ -14,12 +15,12 @@ func TestIsBlockedClientID(t *testing.T) {
clientID := "client-1"
clients := []string{clientID}
a, err := newAccessCtx(clients, nil, nil)
a, err := newAccessCtx(clients, nil, nil, nil)
require.NoError(t, err)
assert.False(t, a.isBlockedClientID(clientID))
a, err = newAccessCtx(nil, clients, nil)
a, err = newAccessCtx(nil, clients, nil, nil)
require.NoError(t, err)
assert.True(t, a.isBlockedClientID(clientID))
@@ -31,7 +32,7 @@ func TestIsBlockedHost(t *testing.T) {
"*.host.com",
"||host3.com^",
"||*^$dnstype=HTTPS",
})
}, nil)
require.NoError(t, err)
testCases := []struct {
@@ -103,58 +104,104 @@ func TestIsBlockedHost(t *testing.T) {
}
}
func TestIsBlockedIP(t *testing.T) {
func TestAccessManager_IsBlockedIP_allow(t *testing.T) {
clients := []string{
"1.2.3.4",
"5.6.7.8/24",
}
allowCtx, err := newAccessCtx(clients, nil, nil)
require.NoError(t, err)
blockCtx, err := newAccessCtx(nil, clients, nil)
privateNets := netutil.SubnetSetFunc(netutil.IsLocallyServed)
allowCtx, err := newAccessCtx(clients, nil, nil, privateNets)
require.NoError(t, err)
testCases := []struct {
ip netip.Addr
name string
wantRule string
wantBlocked bool
ip netip.Addr
want assert.BoolAssertionFunc
name string
wantRule string
}{{
ip: netip.MustParseAddr("1.2.3.4"),
name: "match_ip",
wantRule: "1.2.3.4",
wantBlocked: true,
ip: netip.MustParseAddr("1.2.3.4"),
name: "match_ip",
wantRule: "1.2.3.4",
want: assert.False,
}, {
ip: netip.MustParseAddr("5.6.7.100"),
name: "match_cidr",
wantRule: "5.6.7.8/24",
wantBlocked: true,
ip: netip.MustParseAddr("5.6.7.100"),
name: "match_cidr",
wantRule: "5.6.7.8/24",
want: assert.False,
}, {
ip: netip.MustParseAddr("9.2.3.4"),
name: "no_match_ip",
wantRule: "",
wantBlocked: false,
ip: netip.MustParseAddr("9.2.3.4"),
name: "no_match_ip",
wantRule: "",
want: assert.True,
}, {
ip: netip.MustParseAddr("9.6.7.100"),
name: "no_match_cidr",
wantRule: "",
wantBlocked: false,
ip: netip.MustParseAddr("9.6.7.100"),
name: "no_match_cidr",
wantRule: "",
want: assert.True,
}, {
ip: netip.MustParseAddr("127.0.0.1"),
name: "locally_served_ip",
wantRule: "",
want: assert.False,
}}
t.Run("allow", func(t *testing.T) {
for _, tc := range testCases {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
blocked, rule := allowCtx.isBlockedIP(tc.ip)
assert.Equal(t, !tc.wantBlocked, blocked)
tc.want(t, blocked)
assert.Equal(t, tc.wantRule, rule)
}
})
t.Run("block", func(t *testing.T) {
for _, tc := range testCases {
blocked, rule := blockCtx.isBlockedIP(tc.ip)
assert.Equal(t, tc.wantBlocked, blocked)
assert.Equal(t, tc.wantRule, rule)
}
})
})
}
}
func TestAccessManager_IsBlockedIP_block(t *testing.T) {
clients := []string{
"1.2.3.4",
"5.6.7.8/24",
}
privateNets := netutil.SubnetSetFunc(netutil.IsLocallyServed)
blockCtx, err := newAccessCtx(nil, clients, nil, privateNets)
require.NoError(t, err)
testCases := []struct {
ip netip.Addr
want assert.BoolAssertionFunc
name string
wantRule string
}{{
ip: netip.MustParseAddr("1.2.3.4"),
name: "match_ip",
wantRule: "1.2.3.4",
want: assert.True,
}, {
ip: netip.MustParseAddr("5.6.7.100"),
name: "match_cidr",
wantRule: "5.6.7.8/24",
want: assert.True,
}, {
ip: netip.MustParseAddr("9.2.3.4"),
name: "no_match_ip",
wantRule: "",
want: assert.False,
}, {
ip: netip.MustParseAddr("9.6.7.100"),
name: "no_match_cidr",
wantRule: "",
want: assert.False,
}, {
ip: netip.MustParseAddr("127.0.0.1"),
name: "locally_served_ip",
wantRule: "",
want: assert.False,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
blocked, rule := blockCtx.isBlockedIP(tc.ip)
tc.want(t, blocked)
assert.Equal(t, tc.wantRule, rule)
})
}
}

View File

@@ -509,6 +509,7 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
s.conf.AllowedClients,
s.conf.DisallowedClients,
s.conf.BlockedHosts,
s.privateNets,
)
if err != nil {
return fmt.Errorf("preparing access: %w", err)
@@ -700,10 +701,12 @@ func (s *Server) IsBlockedClient(ip netip.Addr, clientID string) (blocked bool,
blockedByIP := false
if ip != (netip.Addr{}) {
blockedByIP, rule = s.access.isBlockedIP(ip)
log.Debug("by ip %v", blockedByIP)
}
allowlistMode := s.access.allowlistMode()
blockedByClientID := s.access.isBlockedClientID(clientID)
log.Debug("by client id %v", blockedByClientID)
// Allow if at least one of the checks allows in allowlist mode, but block
// if at least one of the checks blocks in blocklist mode.

View File

@@ -555,6 +555,7 @@ func (d *DNSFilter) RegisterFilteringHandlers() {
registerHTTP(http.MethodGet, "/control/rewrite/list", d.handleRewriteList)
registerHTTP(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd)
registerHTTP(http.MethodPut, "/control/rewrite/update", d.handleRewriteUpdate)
registerHTTP(http.MethodPost, "/control/rewrite/delete", d.handleRewriteDelete)
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesIDs)

View File

@@ -6,6 +6,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/log"
"golang.org/x/exp/slices"
)
// TODO(d.kolyshev): Use [rewrite.Item] instead.
@@ -91,3 +92,62 @@ func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request)
d.Config.ConfigModified()
}
// rewriteUpdateJSON is a struct for JSON object with rewrite rule update info.
type rewriteUpdateJSON struct {
Target rewriteEntryJSON `json:"target"`
Update rewriteEntryJSON `json:"update"`
}
// handleRewriteUpdate is the handler for the PUT /control/rewrite/update HTTP
// API.
func (d *DNSFilter) handleRewriteUpdate(w http.ResponseWriter, r *http.Request) {
updateJSON := rewriteUpdateJSON{}
err := json.NewDecoder(r.Body).Decode(&updateJSON)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
rwDel := &LegacyRewrite{
Domain: updateJSON.Target.Domain,
Answer: updateJSON.Target.Answer,
}
rwAdd := &LegacyRewrite{
Domain: updateJSON.Update.Domain,
Answer: updateJSON.Update.Answer,
}
err = rwAdd.normalize()
if err != nil {
// Shouldn't happen currently, since normalize only returns a non-nil
// error when a rewrite is nil, but be change-proof.
aghhttp.Error(r, w, http.StatusBadRequest, "normalizing: %s", err)
return
}
index := -1
defer func() {
if index >= 0 {
d.Config.ConfigModified()
}
}()
d.confLock.Lock()
defer d.confLock.Unlock()
index = slices.IndexFunc(d.Config.Rewrites, rwDel.equal)
if index == -1 {
aghhttp.Error(r, w, http.StatusBadRequest, "target rule not found")
return
}
d.Config.Rewrites = slices.Replace(d.Config.Rewrites, index, index+1, rwAdd)
log.Debug("rewrite: removed element: %s -> %s", rwDel.Domain, rwDel.Answer)
log.Debug("rewrite: added element: %s -> %s", rwAdd.Domain, rwAdd.Answer)
}

View File

@@ -0,0 +1,237 @@
package filtering_test
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TODO(d.kolyshev): Use [rewrite.Item] instead.
type rewriteJSON struct {
Domain string `json:"domain"`
Answer string `json:"answer"`
}
type rewriteUpdateJSON struct {
Target rewriteJSON `json:"target"`
Update rewriteJSON `json:"update"`
}
const (
// testTimeout is the common timeout for tests.
testTimeout = 100 * time.Millisecond
listURL = "/control/rewrite/list"
addURL = "/control/rewrite/add"
deleteURL = "/control/rewrite/delete"
updateURL = "/control/rewrite/update"
decodeErrorMsg = "json.Decode: json: cannot unmarshal string into Go value of type" +
" filtering.rewriteEntryJSON\n"
)
func TestDNSFilter_handleRewriteHTTP(t *testing.T) {
confModCh := make(chan struct{})
reqCh := make(chan struct{})
testRewrites := []*rewriteJSON{
{Domain: "example.local", Answer: "example.rewrite"},
{Domain: "one.local", Answer: "one.rewrite"},
}
testRewritesJSON, mErr := json.Marshal(testRewrites)
require.NoError(t, mErr)
testCases := []struct {
reqData any
name string
url string
method string
wantList []*rewriteJSON
wantBody string
wantConfMod bool
wantStatus int
}{{
name: "list",
url: listURL,
method: http.MethodGet,
reqData: nil,
wantConfMod: false,
wantStatus: http.StatusOK,
wantBody: string(testRewritesJSON) + "\n",
wantList: testRewrites,
}, {
name: "add",
url: addURL,
method: http.MethodPost,
reqData: rewriteJSON{Domain: "add.local", Answer: "add.rewrite"},
wantConfMod: true,
wantStatus: http.StatusOK,
wantBody: "",
wantList: append(
testRewrites,
&rewriteJSON{Domain: "add.local", Answer: "add.rewrite"},
),
}, {
name: "add_error",
url: addURL,
method: http.MethodPost,
reqData: "invalid_json",
wantConfMod: false,
wantStatus: http.StatusBadRequest,
wantBody: decodeErrorMsg,
wantList: testRewrites,
}, {
name: "delete",
url: deleteURL,
method: http.MethodPost,
reqData: rewriteJSON{Domain: "one.local", Answer: "one.rewrite"},
wantConfMod: true,
wantStatus: http.StatusOK,
wantBody: "",
wantList: []*rewriteJSON{{Domain: "example.local", Answer: "example.rewrite"}},
}, {
name: "delete_error",
url: deleteURL,
method: http.MethodPost,
reqData: "invalid_json",
wantConfMod: false,
wantStatus: http.StatusBadRequest,
wantBody: decodeErrorMsg,
wantList: testRewrites,
}, {
name: "update",
url: updateURL,
method: http.MethodPut,
reqData: rewriteUpdateJSON{
Target: rewriteJSON{Domain: "one.local", Answer: "one.rewrite"},
Update: rewriteJSON{Domain: "upd.local", Answer: "upd.rewrite"},
},
wantConfMod: true,
wantStatus: http.StatusOK,
wantBody: "",
wantList: []*rewriteJSON{
{Domain: "example.local", Answer: "example.rewrite"},
{Domain: "upd.local", Answer: "upd.rewrite"},
},
}, {
name: "update_error",
url: updateURL,
method: http.MethodPut,
reqData: "invalid_json",
wantConfMod: false,
wantStatus: http.StatusBadRequest,
wantBody: "json.Decode: json: cannot unmarshal string into Go value of type" +
" filtering.rewriteUpdateJSON\n",
wantList: testRewrites,
}, {
name: "update_error_target",
url: updateURL,
method: http.MethodPut,
reqData: rewriteUpdateJSON{
Target: rewriteJSON{Domain: "inv.local", Answer: "inv.rewrite"},
Update: rewriteJSON{Domain: "upd.local", Answer: "upd.rewrite"},
},
wantConfMod: false,
wantStatus: http.StatusBadRequest,
wantBody: "target rule not found\n",
wantList: testRewrites,
}}
for _, tc := range testCases {
onConfModified := func() {
if !tc.wantConfMod {
panic("config modified has been fired")
}
testutil.RequireSend(testutil.PanicT{}, confModCh, struct{}{}, testTimeout)
}
t.Run(tc.name, func(t *testing.T) {
handlers := make(map[string]http.Handler)
d, err := filtering.New(&filtering.Config{
ConfigModified: onConfModified,
HTTPRegister: func(_, url string, handler http.HandlerFunc) {
handlers[url] = handler
},
Rewrites: rewriteEntriesToLegacyRewrites(testRewrites),
}, nil)
require.NoError(t, err)
t.Cleanup(d.Close)
d.RegisterFilteringHandlers()
require.NotEmpty(t, handlers)
require.Contains(t, handlers, listURL)
require.Contains(t, handlers, tc.url)
var body io.Reader
if tc.reqData != nil {
data, rErr := json.Marshal(tc.reqData)
require.NoError(t, rErr)
body = bytes.NewReader(data)
}
r := httptest.NewRequest(tc.method, tc.url, body)
w := httptest.NewRecorder()
go func() {
handlers[tc.url].ServeHTTP(w, r)
testutil.RequireSend(testutil.PanicT{}, reqCh, struct{}{}, testTimeout)
}()
if tc.wantConfMod {
testutil.RequireReceive(t, confModCh, testTimeout)
}
testutil.RequireReceive(t, reqCh, testTimeout)
assert.Equal(t, tc.wantStatus, w.Code)
respBody, err := io.ReadAll(w.Body)
require.NoError(t, err)
assert.Equal(t, []byte(tc.wantBody), respBody)
assertRewritesList(t, handlers[listURL], tc.wantList)
})
}
}
// assertRewritesList checks if rewrites list equals the list received from the
// handler by listURL.
func assertRewritesList(t *testing.T, handler http.Handler, wantList []*rewriteJSON) {
t.Helper()
r := httptest.NewRequest(http.MethodGet, listURL, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
require.Equal(t, http.StatusOK, w.Code)
var actual []*rewriteJSON
err := json.NewDecoder(w.Body).Decode(&actual)
require.NoError(t, err)
assert.Equal(t, wantList, actual)
}
// rewriteEntriesToLegacyRewrites gets legacy rewrites from json entries.
func rewriteEntriesToLegacyRewrites(entries []*rewriteJSON) (rw []*filtering.LegacyRewrite) {
for _, entry := range entries {
rw = append(rw, &filtering.LegacyRewrite{
Domain: entry.Domain,
Answer: entry.Answer,
})
}
return rw
}

View File

@@ -12,6 +12,18 @@
`GET /control/dhcp/interfaces` HTTP APIs is now correctly set to
`application/json` as opposed to `text/plain`.
### New HTTP API 'PUT /control/rewrite/update'
* The new `PUT /control/rewrite/update` HTTP API allows rewrite rule updates.
It accepts a JSON object with the following format:
```json
{
"target": {"domain":"example.com","answer":"answer-to-update"},
"update": {"domain":"example.com","answer":"new-answer"}
}
```
## v0.107.29: API changes

View File

@@ -1061,6 +1061,17 @@
'responses':
'200':
'description': 'OK.'
'/rewrite/update':
'put':
'tags':
- 'rewrite'
'operationId': 'rewriteUpdate'
'summary': 'Update a Rewrite rule'
'requestBody':
'$ref': '#/components/requestBodies/RewriteUpdate'
'responses':
'200':
'description': 'OK.'
'/i18n/change_language':
'post':
'deprecated': true
@@ -1311,6 +1322,12 @@
'schema':
'$ref': '#/components/schemas/RewriteEntry'
'required': true
'RewriteUpdate':
'content':
'application/json':
'schema':
'$ref': '#/components/schemas/RewriteUpdate'
'required': true
'schemas':
'ServerStatus':
'type': 'object'
@@ -2702,6 +2719,14 @@
'items':
'$ref': '#/components/schemas/RewriteEntry'
'description': 'Rewrite rules array'
'RewriteUpdate':
'type': 'object'
'description': 'Rewrite rule update object'
'properties':
'target':
'$ref': '#/components/schemas/RewriteEntry'
'update':
'$ref': '#/components/schemas/RewriteEntry'
'RewriteEntry':
'type': 'object'
'description': 'Rewrite rule'