diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 5e9fa0d5..5bae787a 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -830,6 +830,36 @@ Request: "private_key_path":"..." // if set, private_key must be empty } +Response: + + 200 OK + +### API: Validate TLS configuration + +Request: + + POST /control/tls/validate + + { + "enabled":true, + "port_https":443, + "port_dns_over_tls":853, + "port_dns_over_quic":784, + "allow_unencrypted_doh":false, + "certificate_chain":"...", + "private_key":"...", + "certificate_path":"...", + "private_key_path":"...", + "valid_cert":true, + "valid_chain":false, + "not_before":"2019-03-19T08:23:45Z", + "not_after":"2029-03-16T08:23:45Z", + "dns_names":null, + "valid_key":true, + "valid_pair":true + } + + Response: 200 OK @@ -1948,6 +1978,29 @@ Check if host name is blocked by SB/PC service: sha256(sub.host.com)[0..1] -> hashes[2],... ... +## API: Get DNS over HTTPS .mobileconfig + +Request: + + GET /apple/doh.mobileconfig + +Response: + + 200 OK + + DOH plist file + +## API: Get DNS over TLS .mobileconfig + +Request: + + GET /apple/dot.mobileconfig + +Response: + + 200 OK + + DOT plist file ## ipset diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 678dd39f..8f2eb5c9 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -249,6 +249,8 @@ "blocking_ipv6": "Blocking IPv6", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "download_mobileconfig_doh": "Download .mobileconfig for DNS-over-HTTPS", + "download_mobileconfig_dot": "Download .mobileconfig for DNS-over-TLS", "plain_dns": "Plain DNS", "form_enter_rate_limit": "Enter rate limit", "rate_limit": "Rate limit", @@ -415,7 +417,8 @@ "dns_privacy": "DNS Privacy", "setup_dns_privacy_1": "<0>DNS-over-TLS: Use <1>{{address}} string.", "setup_dns_privacy_2": "<0>DNS-over-HTTPS: Use <1>{{address}} string.", - "setup_dns_privacy_3": "<0>Please note that encrypted DNS protocols are supported only on Android 9. So you need to install additional software for other operating systems.<0>Here's a list of software you can use.", + "setup_dns_privacy_3": "<0>Here's a list of software you can use.", + "setup_dns_privacy_4": "On an iOS 14 or MacOS Big Sur device you can download special '.mobileconfig' file that adds DNS-over-HTTPS or DNS-over-TLS servers to the DNS settings.", "setup_dns_privacy_android_1": "Android 9 supports DNS-over-TLS natively. To configure it, go to Settings → Network & internet → Advanced → Private DNS and enter your domain name there.", "setup_dns_privacy_android_2": "<0>AdGuard for Android supports <1>DNS-over-HTTPS and <1>DNS-over-TLS.", "setup_dns_privacy_android_3": "<0>Intra adds <1>DNS-over-HTTPS support to Android.", diff --git a/client/src/components/ui/Guide.js b/client/src/components/ui/Guide.js index 0cb738d8..ec629e1b 100644 --- a/client/src/components/ui/Guide.js +++ b/client/src/components/ui/Guide.js @@ -1,10 +1,43 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Trans, withTranslation } from 'react-i18next'; - +import { Trans, useTranslation } from 'react-i18next'; +import i18next from 'i18next'; import Tabs from './Tabs'; import Icons from './Icons'; +const MOBILE_CONFIG_LINKS = { + DOT: '/apple/dot.mobileconfig', + DOH: '/apple/doh.mobileconfig', +}; + +const renderMobileconfigInfo = ({ label, components }) =>
  • + {label} + +
  • ; + +const renderLi = ({ label, components }) =>
  • + { + if (React.isValidElement(props)) { + return props; + } + const { + // eslint-disable-next-line react/prop-types + href, target = '_blank', rel = 'noopener noreferrer', key = '0', + } = props; + + return link; + })}> + {label} + +
  • ; + const dnsPrivacyList = [{ title: 'Android', list: [ @@ -36,6 +69,23 @@ const dnsPrivacyList = [{ { title: 'iOS', list: [ + { + label: 'setup_dns_privacy_ios_2', + components: [ + { + key: 0, + href: 'https://adguard.com/adguard-ios/overview.html', + }, + text, + ], + }, + { + label: 'setup_dns_privacy_4', + components: { + highlight: , + }, + renderComponent: renderMobileconfigInfo, + }, { label: 'setup_dns_privacy_ios_1', components: [ @@ -51,16 +101,6 @@ const dnsPrivacyList = [{ ], }, - { - label: 'setup_dns_privacy_ios_2', - components: [ - { - key: 0, - href: 'https://adguard.com/adguard-ios/overview.html', - }, - text, - ], - }, ], }, { @@ -116,26 +156,15 @@ const dnsPrivacyList = [{ }, ]; -const renderDnsPrivacyList = ({ title, list }) =>
    +const renderDnsPrivacyList = ({ title, list }) =>
    {title} -
      {list.map(({ label, components }) =>
    • - { - if (React.isValidElement(props)) { - return props; - } - const { - // eslint-disable-next-line react/prop-types - href, target = '_blank', rel = 'noopener noreferrer', key = '0', - } = props; - - return link; - })}> - {label} - -
    • )} +
        {list.map( + ({ + label, + components, + renderComponent = renderLi, + }) => renderComponent({ label, components }), + )}
    ; @@ -195,8 +224,8 @@ const getTabs = ({ }, dns_privacy: { title: 'dns_privacy', - // eslint-disable-next-line react/display-name - getTitle: () =>
    + getTitle: function Title() { + return
    {tlsAddress?.length > 0 && (
    @@ -251,14 +280,15 @@ const getTabs = ({ {dnsPrivacyList.map(renderDnsPrivacyList)} }
    -
    , +
    ; + }, }, }); -const renderContent = ({ title, list, getTitle }, t) =>
    -
    {t(title)}
    +const renderContent = ({ title, list, getTitle }) =>
    +
    {i18next.t(title)}
    - {typeof getTitle === 'function' && getTitle()} + {getTitle?.()} {list &&
      {list.map((item) =>
    1. {item} @@ -267,9 +297,10 @@ const renderContent = ({ title, list, getTitle }, t) =>
      ; -const Guide = ({ dnsAddresses, t }) => { - const tlsAddress = (dnsAddresses && dnsAddresses.filter((item) => item.includes('tls://'))) || ''; - const httpsAddress = (dnsAddresses && dnsAddresses.filter((item) => item.includes('https://'))) || ''; +const Guide = ({ dnsAddresses }) => { + const { t } = useTranslation(); + const tlsAddress = dnsAddresses?.filter((item) => item.includes('tls://')) ?? ''; + const httpsAddress = dnsAddresses?.filter((item) => item.includes('https://')) ?? ''; const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1; const [activeTabLabel, setActiveTabLabel] = useState('Router'); @@ -281,7 +312,7 @@ const Guide = ({ dnsAddresses, t }) => { t, }); - const activeTab = renderContent(tabs[activeTabLabel], t); + const activeTab = renderContent(tabs[activeTabLabel]); return (
      @@ -298,12 +329,12 @@ Guide.defaultProps = { Guide.propTypes = { dnsAddresses: PropTypes.array, - t: PropTypes.func.isRequired, }; renderDnsPrivacyList.propTypes = { title: PropTypes.string.isRequired, list: PropTypes.array.isRequired, + renderList: PropTypes.func, }; renderContent.propTypes = { @@ -312,4 +343,11 @@ renderContent.propTypes = { getTitle: PropTypes.func, }; -export default withTranslation()(Guide); +renderLi.propTypes = { + label: PropTypes.string, + components: PropTypes.string, +}; + +renderMobileconfigInfo.propTypes = renderLi.propTypes; + +export default Guide; diff --git a/go.mod b/go.mod index 12616abe..8aaefd0d 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 github.com/miekg/dns v1.1.31 github.com/pkg/errors v0.9.1 + github.com/satori/go.uuid v1.2.0 github.com/sirupsen/logrus v1.6.0 // indirect github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c github.com/stretchr/testify v1.5.1 @@ -31,4 +32,5 @@ require ( google.golang.org/protobuf v1.25.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v2 v2.3.0 + howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 ) diff --git a/go.sum b/go.sum index bfc5b5a2..9b45f53e 100644 --- a/go.sum +++ b/go.sum @@ -210,6 +210,8 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v2.20.3+incompatible h1:0JVooMPsT7A7HqEYdydp/OfjSOYSjhXV7w1hkKj/NPQ= github.com/shirou/gopsutil v2.20.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= @@ -430,6 +432,8 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 h1:AQkaJpH+/FmqRjmXZPELom5zIERYZfwTjnHpfoVMQEc= +howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= diff --git a/home/control.go b/home/control.go index e8b2fa8a..78789aa8 100644 --- a/home/control.go +++ b/home/control.go @@ -97,8 +97,11 @@ func registerControlHandlers() { httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage) http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON))) httpRegister(http.MethodPost, "/control/update", handleUpdate) + httpRegister(http.MethodGet, "/control/profile", handleGetProfile) - httpRegister("GET", "/control/profile", handleGetProfile) + // No auth is necessary for DOH/DOT configurations + http.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoh)) + http.HandleFunc("/apple/dot.mobileconfig", postInstall(handleMobileConfigDot)) RegisterAuthHandlers() } diff --git a/home/dns.go b/home/dns.go index bd70ce38..5328bf96 100644 --- a/home/dns.go +++ b/home/dns.go @@ -197,6 +197,44 @@ func generateServerConfig() dnsforward.ServerConfig { return newconfig } +type DNSEncryption struct { + https string + tls string + quic string +} + +func getDNSEncryption() DNSEncryption { + dnsEncryption := DNSEncryption{} + + tlsConf := tlsConfigSettings{} + + Context.tls.WriteDiskConfig(&tlsConf) + + if tlsConf.Enabled && len(tlsConf.ServerName) != 0 { + + if tlsConf.PortHTTPS != 0 { + addr := tlsConf.ServerName + if tlsConf.PortHTTPS != 443 { + addr = fmt.Sprintf("%s:%d", addr, tlsConf.PortHTTPS) + } + addr = fmt.Sprintf("https://%s/dns-query", addr) + dnsEncryption.https = addr + } + + if tlsConf.PortDNSOverTLS != 0 { + addr := fmt.Sprintf("tls://%s:%d", tlsConf.ServerName, tlsConf.PortDNSOverTLS) + dnsEncryption.tls = addr + } + + if tlsConf.PortDNSOverQUIC != 0 { + addr := fmt.Sprintf("quic://%s:%d", tlsConf.ServerName, tlsConf.PortDNSOverQUIC) + dnsEncryption.quic = addr + } + } + + return dnsEncryption +} + // Get the list of DNS addresses the server is listening on func getDNSAddresses() []string { dnsAddresses := []string{} @@ -217,28 +255,15 @@ func getDNSAddresses() []string { addDNSAddress(&dnsAddresses, config.DNS.BindHost) } - tlsConf := tlsConfigSettings{} - Context.tls.WriteDiskConfig(&tlsConf) - if tlsConf.Enabled && len(tlsConf.ServerName) != 0 { - - if tlsConf.PortHTTPS != 0 { - addr := tlsConf.ServerName - if tlsConf.PortHTTPS != 443 { - addr = fmt.Sprintf("%s:%d", addr, tlsConf.PortHTTPS) - } - addr = fmt.Sprintf("https://%s/dns-query", addr) - dnsAddresses = append(dnsAddresses, addr) - } - - if tlsConf.PortDNSOverTLS != 0 { - addr := fmt.Sprintf("tls://%s:%d", tlsConf.ServerName, tlsConf.PortDNSOverTLS) - dnsAddresses = append(dnsAddresses, addr) - } - - if tlsConf.PortDNSOverQUIC != 0 { - addr := fmt.Sprintf("quic://%s:%d", tlsConf.ServerName, tlsConf.PortDNSOverQUIC) - dnsAddresses = append(dnsAddresses, addr) - } + dnsEncryption := getDNSEncryption() + if dnsEncryption.https != "" { + dnsAddresses = append(dnsAddresses, dnsEncryption.https) + } + if dnsEncryption.tls != "" { + dnsAddresses = append(dnsAddresses, dnsEncryption.tls) + } + if dnsEncryption.quic != "" { + dnsAddresses = append(dnsAddresses, dnsEncryption.quic) } return dnsAddresses diff --git a/home/mobileconfig.go b/home/mobileconfig.go new file mode 100644 index 00000000..e828f117 --- /dev/null +++ b/home/mobileconfig.go @@ -0,0 +1,92 @@ +package home + +import ( + "fmt" + "net/http" + + uuid "github.com/satori/go.uuid" + "howett.net/plist" +) + +type DNSSettings struct { + DNSProtocol string + ServerURL string `plist:",omitempty"` + ServerName string `plist:",omitempty"` +} + +type PayloadContent = struct { + Name string + PayloadDescription string + PayloadDisplayName string + PayloadIdentifier string + PayloadType string + PayloadUUID string + PayloadVersion int + DNSSettings DNSSettings +} + +type MobileConfig = struct { + PayloadContent []PayloadContent + PayloadDescription string + PayloadDisplayName string + PayloadIdentifier string + PayloadRemovalDisallowed bool + PayloadType string + PayloadUUID string + PayloadVersion int +} + +func genUUIDv4() string { + return uuid.NewV4().String() +} + +func getMobileConfig(r *http.Request, d DNSSettings) ([]byte, error) { + name := fmt.Sprintf("%s DNS over %s", r.Host, d.DNSProtocol) + + data := MobileConfig{ + PayloadContent: []PayloadContent{{ + Name: name, + PayloadDescription: "Configures device to use AdGuard Home", + PayloadDisplayName: name, + PayloadIdentifier: fmt.Sprintf("com.apple.dnsSettings.managed.%s", genUUIDv4()), + PayloadType: "com.apple.dnsSettings.managed", + PayloadUUID: genUUIDv4(), + PayloadVersion: 1, + DNSSettings: d, + }}, + PayloadDescription: "Adds AdGuard Home to Big Sur and iOS 14 or newer systems", + PayloadDisplayName: name, + PayloadIdentifier: genUUIDv4(), + PayloadRemovalDisallowed: false, + PayloadType: "Configuration", + PayloadUUID: genUUIDv4(), + PayloadVersion: 1, + } + + return plist.MarshalIndent(data, plist.XMLFormat, "\t") +} + +func handleMobileConfig(w http.ResponseWriter, r *http.Request, d DNSSettings) { + mobileconfig, err := getMobileConfig(r, d) + + if err != nil { + httpError(w, http.StatusInternalServerError, "plist.MarshalIndent: %s", err) + } + + w.Header().Set("Content-Type", "application/xml") + _, _ = w.Write(mobileconfig) +} + +func handleMobileConfigDoh(w http.ResponseWriter, r *http.Request) { + handleMobileConfig(w, r, DNSSettings{ + DNSProtocol: "HTTPS", + ServerURL: fmt.Sprintf("https://%s/dns-query", r.Host), + }) +} + +func handleMobileConfigDot(w http.ResponseWriter, r *http.Request) { + handleMobileConfig(w, r, DNSSettings{ + DNSProtocol: "TLS", + ServerName: r.Host, + }) +} diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index c50d9485..6ad140e2 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -35,6 +35,8 @@ tags: description: AdGuard Home statistics - name: tls description: AdGuard Home HTTPS/DOH/DOT settings + - name: mobileconfig + description: Apple .mobileconfig paths: /status: @@ -915,6 +917,27 @@ paths: application/json: schema: $ref: "#/components/schemas/ProfileInfo" + /apple/doh.mobileconfig: + get: + tags: + - mobileconfig + - global + operationId: mobileConfigDoH + summary: Get DNS over HTTPS .mobileconfig + responses: + "200": + description: DNS over HTTPS plist file + + /apple/dot.mobileconfig: + get: + tags: + - mobileconfig + - global + operationId: mobileConfigDoT + summary: Get TLS over TLS .mobileconfig + responses: + "200": + description: DNS over TLS plist file components: requestBodies: