diff --git a/AGHTechDoc.md b/AGHTechDoc.md index e88bb055..62ea41d4 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -12,6 +12,12 @@ Contents: * Updating * Get version command * Update command +* Device Names and Per-client Settings + * Per-client settings + * Get list of clients + * Add client + * Update client + * Delete client * Enable DHCP server * "Check DHCP" command * "Enable DHCP" command @@ -420,3 +426,135 @@ Step 2. If we would set a different IP address, we'd need to replace the IP address for the current network configuration. But currently this step isn't necessary. ip addr replace dev eth0 192.168.0.1/24 + + +## Device Names and Per-client Settings + +When a client requests information from DNS server, he's identified by IP address. +Administrator can set a name for a client with a known IP and also override global settings for this client. The name is used to improve readability of DNS logs: client's name is shown in UI next to its IP address. The names are loaded from 3 sources: +* automatically from "/etc/hosts" file. It's a list of `IP<->Name` entries which is loaded once on AGH startup from "/etc/hosts" file. +* automatically using rDNS. It's a list of `IP<->Name` entries which is added in runtime using rDNS mechanism when a client first makes a DNS request. +* manually configured via UI. It's a list of client's names and their settings which is loaded from configuration file and stored on disk. + +### Per-client settings + +UI provides means to manage the list of known clients (List/Add/Update/Delete) and their settings. These settings are stored in configuration file as an array of objects. + +Notes: + +* `name`, `ip` and `mac` values are unique. + +* `ip` & `mac` values can't be set both at the same time. + +* If `mac` is set and DHCP server is enabled, IP is taken from DHCP lease table. + +* If `use_global_settings` is true, then DNS responses for this client are processed and filtered using global settings. + +* If `use_global_settings` is false, then the client-specific settings are used to override (disable) global settings. For example, if global setting `parental_enabled` is true, then per-client setting `parental_enabled:false` can disable Parental Control for this specific client. + + +### Get list of clients + +Request: + + GET /control/clients + +Response: + + 200 OK + + { + clients: [ + { + name: "client1" + ip: "..." + mac: "..." + use_global_settings: true + filtering_enabled: false + parental_enabled: false + safebrowsing_enabled: false + safesearch_enabled: false + } + ] + auto_clients: [ + { + name: "host" + ip: "..." + source: "etc/hosts" || "rDNS" + } + ] + } + + +### Add client + +Request: + + POST /control/clients/add + + { + name: "client1" + ip: "..." + mac: "..." + use_global_settings: true + filtering_enabled: false + parental_enabled: false + safebrowsing_enabled: false + safesearch_enabled: false + } + +Response: + + 200 OK + +Error response (Client already exists): + + 400 + + +### Update client + +Request: + + POST /control/clients/update + + { + name: "client1" + data: { + name: "client1" + ip: "..." + mac: "..." + use_global_settings: true + filtering_enabled: false + parental_enabled: false + safebrowsing_enabled: false + safesearch_enabled: false + } + } + +Response: + + 200 OK + +Error response (Client not found): + + 400 + + +### Delete client + +Request: + + POST /control/clients/delete + + { + name: "client1" + } + +Response: + + 200 OK + +Error response (Client not found): + + 400 diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index e0cf54e7..bbfe2336 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -19,6 +19,7 @@ "dhcp_config_saved": "Saved DHCP server config", "form_error_required": "Required field", "form_error_ip_format": "Invalid IPv4 format", + "form_error_mac_format": "Invalid MAC format", "form_error_positive": "Must be greater than 0", "dhcp_form_gateway_input": "Gateway IP", "dhcp_form_subnet_input": "Subnet mask", @@ -105,6 +106,7 @@ "rules_count_table_header": "Rules count", "last_time_updated_table_header": "Last time updated", "actions_table_header": "Actions", + "edit_table_action": "Edit", "delete_table_action": "Delete", "filters_and_hosts": "Filters and hosts blocklists", "filters_and_hosts_hint": "AdGuard Home understands basic adblock rules and hosts files syntax.", @@ -263,5 +265,30 @@ "dns_providers": "Here is a <0>list of known DNS providers to choose from.", "update_now": "Update now", "update_failed": "Auto-update failed. Please follow the steps<\/a> to update manually.", - "processing_update": "Please wait, AdGuard Home is being updated" + "processing_update": "Please wait, AdGuard Home is being updated", + "clients_title": "Clients", + "clients_desc": "Configure devices connected to AdGuard Home", + "settings_global": "Global", + "settings_custom": "Custom", + "table_client": "Client", + "table_name": "Name", + "save_btn": "Save", + "client_add": "Add Client", + "client_new": "New Client", + "client_edit": "Edit Client", + "client_identifier": "Identifier", + "ip_address": "IP address", + "client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server", + "form_enter_ip": "Enter IP", + "form_enter_mac": "Enter MAC", + "form_client_name": "Enter client name", + "client_global_settings": "Use global settings", + "client_deleted": "Client \"{{key}}\" successfully deleted", + "client_added": "Client \"{{key}}\" successfully added", + "client_updated": "Client \"{{key}}\" successfully updated", + "table_statistics": "Requests count (last 24 hours)", + "clients_not_found": "No clients found", + "client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?", + "auto_clients_title": "Clients (runtime)", + "auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration" } \ No newline at end of file diff --git a/client/src/actions/clients.js b/client/src/actions/clients.js new file mode 100644 index 00000000..6af28871 --- /dev/null +++ b/client/src/actions/clients.js @@ -0,0 +1,84 @@ +import { createAction } from 'redux-actions'; +import { t } from 'i18next'; +import Api from '../api/Api'; +import { addErrorToast, addSuccessToast, getClients } from './index'; +import { CLIENT_ID } from '../helpers/constants'; + +const apiClient = new Api(); + +export const toggleClientModal = createAction('TOGGLE_CLIENT_MODAL'); + +export const addClientRequest = createAction('ADD_CLIENT_REQUEST'); +export const addClientFailure = createAction('ADD_CLIENT_FAILURE'); +export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS'); + +export const addClient = config => async (dispatch) => { + dispatch(addClientRequest()); + try { + let data; + if (config.identifier === CLIENT_ID.MAC) { + const { ip, identifier, ...values } = config; + + data = { ...values }; + } else { + const { mac, identifier, ...values } = config; + + data = { ...values }; + } + + await apiClient.addClient(data); + dispatch(addClientSuccess()); + dispatch(toggleClientModal()); + dispatch(addSuccessToast(t('client_added', { key: config.name }))); + dispatch(getClients()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(addClientFailure()); + } +}; + +export const deleteClientRequest = createAction('DELETE_CLIENT_REQUEST'); +export const deleteClientFailure = createAction('DELETE_CLIENT_FAILURE'); +export const deleteClientSuccess = createAction('DELETE_CLIENT_SUCCESS'); + +export const deleteClient = config => async (dispatch) => { + dispatch(deleteClientRequest()); + try { + await apiClient.deleteClient(config); + dispatch(deleteClientSuccess()); + dispatch(addSuccessToast(t('client_deleted', { key: config.name }))); + dispatch(getClients()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(deleteClientFailure()); + } +}; + +export const updateClientRequest = createAction('UPDATE_CLIENT_REQUEST'); +export const updateClientFailure = createAction('UPDATE_CLIENT_FAILURE'); +export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS'); + +export const updateClient = (config, name) => async (dispatch) => { + dispatch(updateClientRequest()); + try { + let data; + if (config.identifier === CLIENT_ID.MAC) { + const { ip, identifier, ...values } = config; + + data = { name, data: { ...values } }; + } else { + const { mac, identifier, ...values } = config; + + data = { name, data: { ...values } }; + } + + await apiClient.updateClient(data); + dispatch(updateClientSuccess()); + dispatch(toggleClientModal()); + dispatch(addSuccessToast(t('client_updated', { key: name }))); + dispatch(getClients()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(updateClientFailure()); + } +}; diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 070c9324..39224388 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -4,7 +4,7 @@ import { t } from 'i18next'; import { showLoading, hideLoading } from 'react-redux-loading-bar'; import axios from 'axios'; -import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers'; +import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea, sortClients } from '../helpers/helpers'; import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants'; import Api from '../api/Api'; @@ -213,14 +213,41 @@ export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS'); export const getClients = () => async (dispatch) => { dispatch(getClientsRequest()); try { - const clients = await apiClient.getGlobalClients(); - dispatch(getClientsSuccess(clients)); + const data = await apiClient.getClients(); + const sortedClients = data.clients && sortClients(data.clients); + const sortedAutoClients = data.auto_clients && sortClients(data.auto_clients); + + dispatch(getClientsSuccess({ + clients: sortedClients || [], + autoClients: sortedAutoClients || [], + })); } catch (error) { dispatch(addErrorToast({ error })); dispatch(getClientsFailure()); } }; +export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST'); +export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE'); +export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS'); + +export const getTopStats = () => async (dispatch, getState) => { + dispatch(getTopStatsRequest()); + const timer = setInterval(async () => { + const state = getState(); + if (state.dashboard.isCoreRunning) { + clearInterval(timer); + try { + const stats = await apiClient.getGlobalStatsTop(); + dispatch(getTopStatsSuccess(stats)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getTopStatsFailure(error)); + } + } + }, 100); +}; + export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST'); export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE'); export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS'); @@ -232,6 +259,7 @@ export const getDnsStatus = () => async (dispatch) => { dispatch(dnsStatusSuccess(dnsStatus)); dispatch(getVersion()); dispatch(getClients()); + dispatch(getTopStats()); } catch (error) { dispatch(addErrorToast({ error })); dispatch(initSettingsFailure()); @@ -289,27 +317,6 @@ export const getStats = () => async (dispatch) => { } }; -export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST'); -export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE'); -export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS'); - -export const getTopStats = () => async (dispatch, getState) => { - dispatch(getTopStatsRequest()); - const timer = setInterval(async () => { - const state = getState(); - if (state.dashboard.isCoreRunning) { - clearInterval(timer); - try { - const stats = await apiClient.getGlobalStatsTop(); - dispatch(getTopStatsSuccess(stats)); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(getTopStatsFailure(error)); - } - } - }, 100); -}; - export const getLogsRequest = createAction('GET_LOGS_REQUEST'); export const getLogsFailure = createAction('GET_LOGS_FAILURE'); export const getLogsSuccess = createAction('GET_LOGS_SUCCESS'); diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 1743cc06..8f34f201 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -39,8 +39,6 @@ export default class Api { GLOBAL_VERSION = { path: 'version.json', method: 'GET' }; GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' }; GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' }; - GLOBAL_CLIENTS = { path: 'clients', method: 'GET' } - GLOBAL_CLIENTS = { path: 'clients', method: 'GET' }; GLOBAL_UPDATE = { path: 'update', method: 'POST' }; restartGlobalFiltering() { @@ -142,11 +140,6 @@ export default class Api { return this.makeRequest(path, method); } - getGlobalClients() { - const { path, method } = this.GLOBAL_CLIENTS; - return this.makeRequest(path, method); - } - getUpdate() { const { path, method } = this.GLOBAL_UPDATE; return this.makeRequest(path, method); @@ -409,4 +402,42 @@ export default class Api { }; return this.makeRequest(path, method, parameters); } + + // Per-client settings + GET_CLIENTS = { path: 'clients', method: 'GET' } + ADD_CLIENT = { path: 'clients/add', method: 'POST' } + DELETE_CLIENT = { path: 'clients/delete', method: 'POST' } + UPDATE_CLIENT = { path: 'clients/update', method: 'POST' } + + getClients() { + const { path, method } = this.GET_CLIENTS; + return this.makeRequest(path, method); + } + + addClient(config) { + const { path, method } = this.ADD_CLIENT; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } + + deleteClient(config) { + const { path, method } = this.DELETE_CLIENT; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } + + updateClient(config) { + const { path, method } = this.UPDATE_CLIENT; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } } diff --git a/client/src/components/App/index.js b/client/src/components/App/index.js index 157a55e6..6e5ed768 100644 --- a/client/src/components/App/index.js +++ b/client/src/components/App/index.js @@ -21,6 +21,7 @@ import Status from '../ui/Status'; import UpdateTopline from '../ui/UpdateTopline'; import UpdateOverlay from '../ui/UpdateOverlay'; import EncryptionTopline from '../ui/EncryptionTopline'; +import Icons from '../ui/Icons'; import i18n from '../../i18n'; class App extends Component { @@ -103,6 +104,7 @@ class App extends Component {