Pull request: home: add bootstrap to mobileconfig, imp code
Updates #3568. Squashed commit of the following: commit ec342e6223e2b2efe9a8bf833d5406a44c6417e4 Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Mon Sep 13 15:16:07 2021 +0300 home: imp tests commit 67cd771e631938d3e8a5340315314210de796174 Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Mon Sep 13 14:34:03 2021 +0300 home: add bootstrap to mobileconfig, imp code
This commit is contained in:
@@ -162,8 +162,10 @@ type tlsConfigSettings struct {
|
||||
dnsforward.TLSConfig `yaml:",inline" json:",inline"`
|
||||
}
|
||||
|
||||
// initialize to default values, will be changed later when reading config or parsing command line
|
||||
var config = configuration{
|
||||
// config is the global configuration structure.
|
||||
//
|
||||
// TODO(a.garipov, e.burkov): This global is afwul and must be removed.
|
||||
var config = &configuration{
|
||||
BindPort: 3000,
|
||||
BetaBindPort: 0,
|
||||
BindHost: net.IP{0, 0, 0, 0},
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
// getAddrsResponse is the response for /install/get_addresses endpoint.
|
||||
@@ -286,55 +286,29 @@ func shutdownSrv(ctx context.Context, cancel context.CancelFunc, srv *http.Serve
|
||||
|
||||
// Apply new configuration, start DNS server, restart Web server
|
||||
func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
req := applyConfigReq{}
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
req, restartHTTP, err := decodeApplyConfigReq(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err)
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if req.Web.Port == 0 || req.DNS.Port == 0 {
|
||||
httpError(w, http.StatusBadRequest, "port value can't be 0")
|
||||
return
|
||||
}
|
||||
|
||||
restartHTTP := true
|
||||
if config.BindHost.Equal(req.Web.IP) && config.BindPort == req.Web.Port {
|
||||
// no need to rebind
|
||||
restartHTTP = false
|
||||
}
|
||||
|
||||
// validate that hosts and ports are bindable
|
||||
if restartHTTP {
|
||||
err = aghnet.CheckPortAvailable(req.Web.IP, req.Web.Port)
|
||||
if err != nil {
|
||||
httpError(
|
||||
w,
|
||||
http.StatusBadRequest,
|
||||
"can not listen on IP:port %s: %s",
|
||||
netutil.JoinHostPort(req.Web.IP.String(), req.Web.Port),
|
||||
err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = aghnet.CheckPacketPortAvailable(req.DNS.IP, req.DNS.Port)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = aghnet.CheckPortAvailable(req.DNS.IP, req.DNS.Port)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var curConfig configuration
|
||||
copyInstallSettings(&curConfig, &config)
|
||||
var curConfig *configuration
|
||||
copyInstallSettings(curConfig, config)
|
||||
|
||||
Context.firstRun = false
|
||||
config.BindHost = req.Web.IP
|
||||
@@ -349,8 +323,9 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
err = StartMods()
|
||||
if err != nil {
|
||||
Context.firstRun = true
|
||||
copyInstallSettings(&config, &curConfig)
|
||||
copyInstallSettings(config, curConfig)
|
||||
httpError(w, http.StatusInternalServerError, "%s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -361,8 +336,9 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
err = config.write()
|
||||
if err != nil {
|
||||
Context.firstRun = true
|
||||
copyInstallSettings(&config, &curConfig)
|
||||
copyInstallSettings(config, curConfig)
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't write config: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -387,6 +363,36 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// decodeApplyConfigReq decodes the configuration, validates some parameters,
|
||||
// and returns it along with the boolean indicating whether or not the HTTP
|
||||
// server must be restarted.
|
||||
func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, err error) {
|
||||
req = &applyConfigReq{}
|
||||
err = json.NewDecoder(r).Decode(&req)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("parsing request: %w", err)
|
||||
}
|
||||
|
||||
if req.Web.Port == 0 || req.DNS.Port == 0 {
|
||||
return nil, false, errors.Error("ports cannot be 0")
|
||||
}
|
||||
|
||||
restartHTTP = !config.BindHost.Equal(req.Web.IP) || config.BindPort != req.Web.Port
|
||||
if restartHTTP {
|
||||
err = aghnet.CheckPortAvailable(req.Web.IP, req.Web.Port)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf(
|
||||
"checking address %s:%d: %w",
|
||||
req.Web.IP.String(),
|
||||
req.Web.Port,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return req, restartHTTP, err
|
||||
}
|
||||
|
||||
func (web *Web) registerInstallHandlers() {
|
||||
Context.mux.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses)))
|
||||
Context.mux.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig)))
|
||||
|
||||
@@ -349,7 +349,7 @@ func run(args options, clientBuildFS fs.FS) {
|
||||
|
||||
setupContext(args)
|
||||
|
||||
err = configureOS(&config)
|
||||
err = configureOS(config)
|
||||
fatalOnError(err)
|
||||
|
||||
// clients package uses filtering package's static data (filtering.BlockedSvcKnown()),
|
||||
|
||||
@@ -10,35 +10,60 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"howett.net/plist"
|
||||
)
|
||||
|
||||
// dnsSettings is the DNSSetting.DNSSettings mobileconfig profile.
|
||||
//
|
||||
// See https://developer.apple.com/documentation/devicemanagement/dnssettings/dnssettings.
|
||||
type dnsSettings struct {
|
||||
// DNSProtocol is the required protocol to be used. The valid values
|
||||
// are "HTTPS" and "TLS".
|
||||
DNSProtocol string
|
||||
ServerURL string `plist:",omitempty"`
|
||||
ServerName string `plist:",omitempty"`
|
||||
clientID string
|
||||
|
||||
// ServerURL is the URI template of the DoH server. It must be empty if
|
||||
// DNSProtocol is not "HTTPS".
|
||||
ServerURL string `plist:",omitempty"`
|
||||
|
||||
// ServerName is the hostname of the DoT server. It must be empty if
|
||||
// DNSProtocol is not "TLS".
|
||||
ServerName string `plist:",omitempty"`
|
||||
|
||||
// ServerAddresses is a list of plain DNS server IP addresses used to
|
||||
// resolve the hostname in ServerURL or ServerName.
|
||||
ServerAddresses []string `plist:",omitempty"`
|
||||
}
|
||||
|
||||
// payloadContent is a Device Management Profile payload.
|
||||
//
|
||||
// See https://developer.apple.com/documentation/devicemanagement/configuring_multiple_devices_using_profiles#3234127.
|
||||
type payloadContent struct {
|
||||
Name string
|
||||
PayloadDescription string
|
||||
PayloadDisplayName string
|
||||
PayloadIdentifier string
|
||||
DNSSettings *dnsSettings
|
||||
|
||||
PayloadType string
|
||||
PayloadIdentifier string
|
||||
PayloadUUID string
|
||||
DNSSettings dnsSettings
|
||||
PayloadDisplayName string
|
||||
PayloadDescription string
|
||||
PayloadVersion int
|
||||
}
|
||||
|
||||
// dnsSettingsPayloadType is the payload type for a DNSSettings profile.
|
||||
const dnsSettingsPayloadType = "com.apple.dnsSettings.managed"
|
||||
|
||||
// mobileConfig contains the TopLevel properties for configuring Device
|
||||
// Management Profiles.
|
||||
//
|
||||
// See https://developer.apple.com/documentation/devicemanagement/toplevel.
|
||||
type mobileConfig struct {
|
||||
PayloadDescription string
|
||||
PayloadDisplayName string
|
||||
PayloadIdentifier string
|
||||
PayloadType string
|
||||
PayloadUUID string
|
||||
PayloadContent []payloadContent
|
||||
PayloadContent []*payloadContent
|
||||
PayloadVersion int
|
||||
PayloadRemovalDisallowed bool
|
||||
}
|
||||
@@ -52,7 +77,7 @@ const (
|
||||
dnsProtoTLS = "TLS"
|
||||
)
|
||||
|
||||
func getMobileConfig(d dnsSettings) ([]byte, error) {
|
||||
func encodeMobileConfig(d *dnsSettings, clientID string) ([]byte, error) {
|
||||
var dspName string
|
||||
switch proto := d.DNSProtocol; proto {
|
||||
case dnsProtoHTTPS:
|
||||
@@ -60,41 +85,41 @@ func getMobileConfig(d dnsSettings) ([]byte, error) {
|
||||
u := &url.URL{
|
||||
Scheme: schemeHTTPS,
|
||||
Host: d.ServerName,
|
||||
Path: path.Join("/dns-query", d.clientID),
|
||||
Path: path.Join("/dns-query", clientID),
|
||||
}
|
||||
d.ServerURL = u.String()
|
||||
|
||||
// Empty the ServerName field since it is only must be presented
|
||||
// in DNS-over-TLS configuration.
|
||||
//
|
||||
// See https://developer.apple.com/documentation/devicemanagement/dnssettings/dnssettings.
|
||||
d.ServerName = ""
|
||||
case dnsProtoTLS:
|
||||
dspName = fmt.Sprintf("%s DoT", d.ServerName)
|
||||
if d.clientID != "" {
|
||||
d.ServerName = d.clientID + "." + d.ServerName
|
||||
if clientID != "" {
|
||||
d.ServerName = clientID + "." + d.ServerName
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("bad dns protocol %q", proto)
|
||||
}
|
||||
|
||||
data := mobileConfig{
|
||||
PayloadContent: []payloadContent{{
|
||||
Name: dspName,
|
||||
PayloadDescription: "Configures device to use AdGuard Home",
|
||||
PayloadDisplayName: dspName,
|
||||
PayloadIdentifier: fmt.Sprintf("com.apple.dnsSettings.managed.%s", genUUIDv4()),
|
||||
PayloadType: "com.apple.dnsSettings.managed",
|
||||
payloadID := fmt.Sprintf("%s.%s", dnsSettingsPayloadType, genUUIDv4())
|
||||
data := &mobileConfig{
|
||||
PayloadDescription: "Adds AdGuard Home to macOS Big Sur " +
|
||||
"and iOS 14 or newer systems",
|
||||
PayloadDisplayName: dspName,
|
||||
PayloadIdentifier: genUUIDv4(),
|
||||
PayloadType: "Configuration",
|
||||
PayloadUUID: genUUIDv4(),
|
||||
PayloadContent: []*payloadContent{{
|
||||
PayloadType: dnsSettingsPayloadType,
|
||||
PayloadIdentifier: payloadID,
|
||||
PayloadUUID: genUUIDv4(),
|
||||
PayloadDisplayName: dspName,
|
||||
PayloadDescription: "Configures device to use AdGuard Home",
|
||||
PayloadVersion: 1,
|
||||
DNSSettings: d,
|
||||
}},
|
||||
PayloadDescription: "Adds AdGuard Home to Big Sur and iOS 14 or newer systems",
|
||||
PayloadDisplayName: dspName,
|
||||
PayloadIdentifier: genUUIDv4(),
|
||||
PayloadRemovalDisallowed: false,
|
||||
PayloadType: "Configuration",
|
||||
PayloadUUID: genUUIDv4(),
|
||||
PayloadVersion: 1,
|
||||
PayloadRemovalDisallowed: false,
|
||||
}
|
||||
|
||||
return plist.MarshalIndent(data, plist.XMLFormat, "\t")
|
||||
@@ -133,13 +158,13 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
|
||||
}
|
||||
}
|
||||
|
||||
d := dnsSettings{
|
||||
DNSProtocol: dnsp,
|
||||
ServerName: host,
|
||||
clientID: clientID,
|
||||
d := &dnsSettings{
|
||||
DNSProtocol: dnsp,
|
||||
ServerName: host,
|
||||
ServerAddresses: cloneBootstrap(),
|
||||
}
|
||||
|
||||
mobileconfig, err := getMobileConfig(d)
|
||||
mobileconfig, err := encodeMobileConfig(d, clientID)
|
||||
if err != nil {
|
||||
respondJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
|
||||
@@ -163,6 +188,14 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
|
||||
_, _ = w.Write(mobileconfig)
|
||||
}
|
||||
|
||||
// cloneBootstrap returns a clone of the current bootstrap DNS servers.
|
||||
func cloneBootstrap() (bootstrap []string) {
|
||||
config.RLock()
|
||||
defer config.RUnlock()
|
||||
|
||||
return stringutil.CloneSlice(config.DNS.BootstrapDNS)
|
||||
}
|
||||
|
||||
func handleMobileConfigDoH(w http.ResponseWriter, r *http.Request) {
|
||||
handleMobileConfig(w, r, dnsProtoHTTPS)
|
||||
}
|
||||
|
||||
@@ -7,12 +7,39 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"howett.net/plist"
|
||||
)
|
||||
|
||||
// testBootstrapDNS are the bootstrap plain DNS server addresses for tests.
|
||||
var testBootstrapDNS = []string{
|
||||
"94.140.14.14",
|
||||
"94.140.15.15",
|
||||
}
|
||||
|
||||
// setupBootstraps is a helper that sets up the bootstrap plain DNS server
|
||||
// configuration for tests and also tears it down in a cleanup function.
|
||||
func setupBootstraps(t testing.TB) {
|
||||
t.Helper()
|
||||
|
||||
prevConfig := config
|
||||
t.Cleanup(func() {
|
||||
config = prevConfig
|
||||
})
|
||||
config = &configuration{
|
||||
DNS: dnsConfig{
|
||||
FilteringConfig: dnsforward.FilteringConfig{
|
||||
BootstrapDNS: testBootstrapDNS,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMobileConfigDoH(t *testing.T) {
|
||||
setupBootstraps(t)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig?host=example.org", nil)
|
||||
require.NoError(t, err)
|
||||
@@ -25,11 +52,16 @@ func TestHandleMobileConfigDoH(t *testing.T) {
|
||||
var mc mobileConfig
|
||||
_, err = plist.Unmarshal(w.Body.Bytes(), &mc)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, mc.PayloadContent, 1)
|
||||
assert.Equal(t, "example.org DoH", mc.PayloadContent[0].Name)
|
||||
|
||||
assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName)
|
||||
assert.Equal(t, "https://example.org/dns-query", mc.PayloadContent[0].DNSSettings.ServerURL)
|
||||
|
||||
s := mc.PayloadContent[0].DNSSettings
|
||||
require.NotNil(t, s)
|
||||
|
||||
assert.Equal(t, testBootstrapDNS, s.ServerAddresses)
|
||||
assert.Empty(t, s.ServerName)
|
||||
assert.Equal(t, "https://example.org/dns-query", s.ServerURL)
|
||||
})
|
||||
|
||||
t.Run("error_no_host", func(t *testing.T) {
|
||||
@@ -66,15 +98,22 @@ func TestHandleMobileConfigDoH(t *testing.T) {
|
||||
var mc mobileConfig
|
||||
_, err = plist.Unmarshal(w.Body.Bytes(), &mc)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, mc.PayloadContent, 1)
|
||||
assert.Equal(t, "example.org DoH", mc.PayloadContent[0].Name)
|
||||
|
||||
assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName)
|
||||
assert.Equal(t, "https://example.org/dns-query/cli42", mc.PayloadContent[0].DNSSettings.ServerURL)
|
||||
|
||||
s := mc.PayloadContent[0].DNSSettings
|
||||
require.NotNil(t, s)
|
||||
|
||||
assert.Equal(t, testBootstrapDNS, s.ServerAddresses)
|
||||
assert.Empty(t, s.ServerName)
|
||||
assert.Equal(t, "https://example.org/dns-query/cli42", s.ServerURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleMobileConfigDoT(t *testing.T) {
|
||||
setupBootstraps(t)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig?host=example.org", nil)
|
||||
require.NoError(t, err)
|
||||
@@ -87,11 +126,16 @@ func TestHandleMobileConfigDoT(t *testing.T) {
|
||||
var mc mobileConfig
|
||||
_, err = plist.Unmarshal(w.Body.Bytes(), &mc)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, mc.PayloadContent, 1)
|
||||
assert.Equal(t, "example.org DoT", mc.PayloadContent[0].Name)
|
||||
|
||||
assert.Equal(t, "example.org DoT", mc.PayloadContent[0].PayloadDisplayName)
|
||||
assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName)
|
||||
|
||||
s := mc.PayloadContent[0].DNSSettings
|
||||
require.NotNil(t, s)
|
||||
|
||||
assert.Equal(t, testBootstrapDNS, s.ServerAddresses)
|
||||
assert.Equal(t, "example.org", s.ServerName)
|
||||
assert.Empty(t, s.ServerURL)
|
||||
})
|
||||
|
||||
t.Run("error_no_host", func(t *testing.T) {
|
||||
@@ -129,10 +173,15 @@ func TestHandleMobileConfigDoT(t *testing.T) {
|
||||
var mc mobileConfig
|
||||
_, err = plist.Unmarshal(w.Body.Bytes(), &mc)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, mc.PayloadContent, 1)
|
||||
assert.Equal(t, "example.org DoT", mc.PayloadContent[0].Name)
|
||||
|
||||
assert.Equal(t, "example.org DoT", mc.PayloadContent[0].PayloadDisplayName)
|
||||
assert.Equal(t, "cli42.example.org", mc.PayloadContent[0].DNSSettings.ServerName)
|
||||
|
||||
s := mc.PayloadContent[0].DNSSettings
|
||||
require.NotNil(t, s)
|
||||
|
||||
assert.Equal(t, testBootstrapDNS, s.ServerAddresses)
|
||||
assert.Equal(t, "cli42.example.org", s.ServerName)
|
||||
assert.Empty(t, s.ServerURL)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user