From 4473b90a2aec8a1dc8d1295529a21dba8abec3c1 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Tue, 9 Sep 2025 13:31:58 +0700 Subject: [PATCH 1/5] added LICENSE and README.md --- LICENSE | 21 ++++++ README.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2a20de9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 DNS Service Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..818d5a8 --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +# DNS Service + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +A lightweight DNS management service written in C with a web-based interface for configuring DNS settings and managing dnsmasq configuration. + +## Features + +- **Web Interface**: Modern responsive web UI for DNS configuration +- **DNS Settings Management**: Configure upstream DNS servers, local domains, and cache settings +- **DHCP Configuration**: Enable/disable DHCP with range configuration +- **File Management**: Direct editing of configuration files +- **SSL/TLS Support**: Optional HTTPS with certificate configuration +- **JSON Configuration**: Easy-to-manage JSON-based configuration +- **Cross-platform**: Supports multiple build systems (Make, CMake, Meson) + +## Architecture + +The service consists of: +- **HTTP/HTTPS Server**: Built on Mongoose web server +- **REST API**: JSON-based API for configuration management +- **Web Interface**: Bootstrap-based responsive UI +- **Configuration Parser**: Custom dnsmasq config parser +- **File Operations**: Safe file reading/writing utilities + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/settings` | Get current DNS settings | +| POST | `/api/settings` | Update DNS settings | +| GET | `/api/settings/file` | Get raw configuration file | +| POST | `/api/settings/file` | Update configuration file | +| GET | `/#` | Serve web interface | + +## Requirements + +### Build Dependencies +- GCC compiler with C23 support +- OpenSSL development libraries +- pthread library + +### System Dependencies +- dnsmasq (for DNS service functionality) +- SSL certificates (for HTTPS support) + +## Installation + +### Using Make +```bash +make +``` + +### Using CMake +```bash +mkdir build && cd build +cmake .. +make +``` + +### Using Meson +```bash +meson setup builddir +meson compile -C builddir +``` + +## Configuration + +Edit `config.json` to configure the service: + +```json +{ + "address": "0.0.0.0:8321", + "tls_address": "0.0.0.0:8322", + "web": "./web", + "tls_enabled": true, + "tls_cert": "/path/to/certificate.crt", + "tls_key": "/path/to/private.key", + "system_initialization": "rc", + "dnsmasq_config": "/etc/dnsmasq.conf" +} +``` + +### Configuration Parameters + +- `address`: HTTP server bind address and port +- `tls_address`: HTTPS server bind address and port +- `web`: Path to web interface files +- `tls_enabled`: Enable/disable HTTPS support +- `tls_cert`: Path to SSL certificate file +- `tls_key`: Path to SSL private key file +- `system_initialization`: System initialization type +- `dnsmasq_config`: Path to dnsmasq configuration file + +## Usage + +### Starting the Service +```bash +./dns_service +``` + +The service will start HTTP server on the configured address (default: `0.0.0.0:8321`) and optionally HTTPS server if TLS is enabled. + +### Web Interface + +Navigate to `http://localhost:8321` (or your configured address) to access the web interface where you can: + +- Configure upstream DNS servers +- Manage local domain mappings +- Enable/disable DHCP with range settings +- Adjust DNS cache size +- Edit configuration files directly + +### API Usage + +#### Get Current Settings +```bash +curl -X GET http://localhost:8321/api/settings +``` + +#### Update Settings +```bash +curl -X POST http://localhost:8321/api/settings \ + -H "Content-Type: application/json" \ + -d '{"cache_size": 1000, "resolv": ["8.8.8.8", "8.8.4.4"]}' +``` + +## Development + +### Project Structure +``` +dns_service_c/ +├── src/ +│ ├── lib/ # Third-party libraries +│ │ ├── mongoose.c # HTTP server +│ │ ├── cJSON.c # JSON parser +│ │ └── ... +│ ├── main.c # Application entry point +│ ├── config.c # Configuration management +│ ├── settings.c # DNS settings handlers +│ └── ... +├── web/ # Web interface files +│ ├── index.html +│ ├── css/ +│ └── js/ +├── config.json # Service configuration +└── README.md +``` + +### Building for Development +```bash +# With debug symbols +make CFLAGS="-Wall -g -std=gnu2x -DDEBUG" + +# Clean build +make clean && make +``` + +### Code Style +The project uses `.clang-format` for consistent code formatting: +```bash +clang-format -i src/*.c src/*.h +``` + +## Security Considerations + +- The service runs with system privileges to modify DNS configuration +- HTTPS is strongly recommended for production deployments +- Ensure proper file permissions on certificate files +- Consider firewall rules to restrict access to management interface + +## Troubleshooting + +### Common Issues + +1. **Permission denied when accessing dnsmasq config** + - Ensure the service has read/write permissions to the dnsmasq configuration file + - Run with appropriate privileges or adjust file permissions + +2. **SSL/TLS certificate errors** + - Verify certificate and key file paths in `config.json` + - Ensure certificate files have correct permissions + - Check certificate validity and format + +3. **Port binding errors** + - Verify ports are not already in use + - Check firewall settings + - Ensure proper privileges for binding to privileged ports + +### Logging +The service uses Mongoose's built-in logging. Set log level in code or use debug build for verbose output. + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes following the existing code style +4. Test your changes thoroughly +5. Submit a pull request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +### Third-party Libraries + +This project includes the following third-party libraries: + +- **Mongoose**: HTTP/WebSocket server library +- **cJSON**: JSON parser library + +Please refer to their respective licenses for usage terms. + +## Support + +For issues and questions, please check the project's issue tracker or documentation. \ No newline at end of file From 6e7e8b19eb9a6c37fd31d8da847a4afbb59ffdba Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Tue, 9 Sep 2025 14:02:21 +0700 Subject: [PATCH 2/5] added restart_dnsmasq and change set_settings_file --- config.json | 16 +-- src/restart_dnsmasq.c | 32 +++++ src/restart_dnsmasq.h | 3 + src/set_settings_file.c | 49 ++++++-- web/js/settings/settings.js | 238 ++++++++++++++++++------------------ 5 files changed, 203 insertions(+), 135 deletions(-) create mode 100644 src/restart_dnsmasq.c create mode 100644 src/restart_dnsmasq.h diff --git a/config.json b/config.json index f79a114..0c05b20 100644 --- a/config.json +++ b/config.json @@ -1,10 +1,10 @@ { - "address": "0.0.0.0:8321", - "tls_address": "0.0.0.0:8322", - "web": "./web", - "tls_enabled": true, - "tls_cert": "/home/tola/certs/localhost/localhost.crt", - "tls_key": "/home/tola/certs/localhost/localhost.key", - "system_initialization": "rc", - "dnsmasq_config": "/etc/dnsmasq.conf" + "address": "0.0.0.0:8321", + "tls_address": "0.0.0.0:8322", + "web": "./web", + "tls_enabled": true, + "tls_cert": "/home/tola/certs/localhost/localhost.crt", + "tls_key": "/home/tola/certs/localhost/localhost.key", + "system_initialization": "systemd", + "dnsmasq_config": "/etc/dnsmasq.conf" } diff --git a/src/restart_dnsmasq.c b/src/restart_dnsmasq.c new file mode 100644 index 0000000..83990fd --- /dev/null +++ b/src/restart_dnsmasq.c @@ -0,0 +1,32 @@ +#include "restart_dnsmasq.h" +#include "ConfigManager.h" +#include "config.h" +#include +#include + +char *restart_dnsmasq() { + int res = -2; + char *system_initialization = cm_get_parameter_as_string(config, "system_initialization"); + if (!system_initialization) { + res = -1; + } + if (!strcmp(system_initialization, "rc")) { + res = system("rc restart dnsmasq"); + } + if (!strcmp(system_initialization, "openrc")) { + res = system("rc restart dnsmasq"); + } + if (!strcmp(system_initialization, "systemd")) { + res = system("systemctl restart dnsmasq"); + } + if (!strcmp(system_initialization, "finit")) { + res = system("initctl restart dnsmasq"); + } + if (!strcmp(system_initialization, "init")) { + res = system("/etc/init.d/dnsmasq restart"); + } + if (res != 0) { + return strdup("Failed to restart dnsmasq"); + } + return nullptr; +} diff --git a/src/restart_dnsmasq.h b/src/restart_dnsmasq.h new file mode 100644 index 0000000..d434892 --- /dev/null +++ b/src/restart_dnsmasq.h @@ -0,0 +1,3 @@ +#pragma once + +char *restart_dnsmasq(); diff --git a/src/set_settings_file.c b/src/set_settings_file.c index c13bb0c..75b8d0a 100644 --- a/src/set_settings_file.c +++ b/src/set_settings_file.c @@ -1,21 +1,52 @@ -#include "settings_file.h" #include +#include "cJSON.h" #include "config.h" +#include "restart_dnsmasq.h" +#include "settings_file.h" void set_settings_file(struct mg_connection *c, struct mg_http_message *hm) { char *filename = cm_get_parameter_as_string(config, "dnsmasq_config"); if (!filename) { - mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), - MG_ESC("can't read config file")); - return; + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("can't read config file")); + return; } - char *res = write_file(cm_get_parameter_as_string(config, "dnsmasq_config"), hm->body.buf); + char *res = write_file(cm_get_parameter_as_string(config, "dnsmasq_config"), + hm->body.buf); if (!res) { - mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), - MG_ESC("can't write file")); - return; + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("can't write file")); + return; } - mg_http_reply(c, 200, "Content-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n", res); + char *error = restart_dnsmasq(); + if (error) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC(error)); + free(error); + return; + } + cJSON *response = cJSON_CreateObject(); + if (!response) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("can't create response")); + free(filename); + free(res); + return; + } + if (!cJSON_AddStringToObject(response, "data", res)) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("can't add data to response")); + cJSON_Delete(response); + free(filename); + free(res); + return; + } + char *response_str = cJSON_PrintUnformatted(response); + mg_http_reply( + c, 200, "Content-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n", + response_str); + cJSON_Delete(response); + free(response_str); free(filename); free(res); } diff --git a/web/js/settings/settings.js b/web/js/settings/settings.js index e45e713..a8ba708 100644 --- a/web/js/settings/settings.js +++ b/web/js/settings/settings.js @@ -1,207 +1,209 @@ const upstream_dns_servers_table_body = document.querySelector( - "#upstream-dns-servers-table-body", + "#upstream-dns-servers-table-body", ); const local_domains_table_body = document.querySelector( - "#local-domains-table-body", + "#local-domains-table-body", ); const cache_size_input = document.querySelector("#cache-size-input"); const save_settings_button = document.querySelector("#save-settings-button"); const add_dns_server_button = document.querySelector("#add_dns_server_button"); const add_local_domain_button = document.querySelector( - "#add_local_domain_button", + "#add_local_domain_button", ); const edit_file_input = document.querySelector("#edit_file_input"); const save_config_file_button = document.querySelector( - "#save_config_file_button", + "#save_config_file_button", ); var settings = {}; var configFile = ""; const updateDnsServersTable = () => { - upstream_dns_servers_table_body.innerHTML = ""; - for (var i = 0; i < settings.resolv.length; i++) { - upstream_dns_servers_table_body.innerHTML += ` + upstream_dns_servers_table_body.innerHTML = ""; + for (var i = 0; i < settings.resolv.length; i++) { + upstream_dns_servers_table_body.innerHTML += ` ${settings.resolv[i]} `; - } + } }; const updateLocalDomainsTable = () => { - local_domains_table_body.innerHTML = ""; - for (const [key, value] of Object.entries(settings.hosts)) { - if (Array.isArray(value)) { - for (var i = 0; i < value.length; i++) { - local_domains_table_body.innerHTML += ` + local_domains_table_body.innerHTML = ""; + for (const [key, value] of Object.entries(settings.hosts)) { + if (Array.isArray(value)) { + for (var i = 0; i < value.length; i++) { + local_domains_table_body.innerHTML += ` ${key} ${value[i]} `; - } - } else { - local_domains_table_body.innerHTML += ` + } + } else { + local_domains_table_body.innerHTML += ` ${key} ${value} `; - } } + } }; const updateInterface = () => { - updateDnsServersTable(); - updateLocalDomainsTable(); - cache_size_input.value = settings.cache_size; + updateDnsServersTable(); + updateLocalDomainsTable(); + cache_size_input.value = settings.cache_size; }; const getSettings = () => { - fetch(window.location.origin + "/api/settings") - .then((data) => data.json()) - .then((jsondata) => { - settings = jsondata; - updateInterface(); - }) - .catch((e) => alert(e)); + fetch(window.location.origin + "/api/settings") + .then((data) => data.json()) + .then((jsondata) => { + settings = jsondata; + updateInterface(); + }) + .catch((e) => alert(e)); }; const setSettings = () => { - fetch(window.location.origin + "/api/settings", { - method: "POST", - body: JSON.stringify(settings), + fetch(window.location.origin + "/api/settings", { + method: "POST", + body: JSON.stringify(settings), + }) + .then((data) => data.json()) + .then((jsondata) => { + settings = jsondata; + updateInterface(); + alert("Success set settings"); }) - .then((data) => data.json()) - .then((jsondata) => { - settings = jsondata; - updateInterface(); - alert("Success set settings"); - }) - .catch((e) => alert(e)); + .catch((e) => alert(e)); }; save_settings_button.addEventListener("click", () => { - setSettings(); + setSettings(); }); const write_cache_size = (value) => { - settings.cache_size = value; + settings.cache_size = value; }; cache_size_input.addEventListener("change", (e) => { - write_cache_size(e.target.value); + write_cache_size(e.target.value); }); cache_size_input.addEventListener("input", (e) => { - write_cache_size(e.target.value); + write_cache_size(e.target.value); }); getSettings(); add_dns_server_button.addEventListener("click", () => { - const dns_server = prompt("Input your DNS server:"); - if (dns_server === null) return; - if (confirm("Confirm DNS settings? (add DNS server)")) { - if (Array.isArray(settings.resolv)) { - if (!settings.resolv.includes(dns_server)) { - settings.resolv.push(dns_server); - updateDnsServersTable(); - } - } + const dns_server = prompt("Input your DNS server:"); + if (dns_server === null) return; + if (confirm("Confirm DNS settings? (add DNS server)")) { + if (Array.isArray(settings.resolv)) { + if (!settings.resolv.includes(dns_server)) { + settings.resolv.push(dns_server); + updateDnsServersTable(); + } } + } }); upstream_dns_servers_table_body.addEventListener("click", (e) => { - if (e.target.classList.contains("delete-table-button")) { - if ( - confirm(`Confirm DNS settings? (delete DNS server: ${e.target.id})`) - ) { - if (Array.isArray(settings.resolv)) { - settings.resolv.splice(e.target.id, 1); - updateDnsServersTable(); - } - } + if (e.target.classList.contains("delete-table-button")) { + if (confirm(`Confirm DNS settings? (delete DNS server: ${e.target.id})`)) { + if (Array.isArray(settings.resolv)) { + settings.resolv.splice(e.target.id, 1); + updateDnsServersTable(); + } } + } }); add_local_domain_button.addEventListener("click", () => { - const domainip = prompt("Input localdomain IP:"); - if (domainip === null) return; - const domain = prompt("Input domain:"); - if (domainip === null || domain === null) return; - if ( - confirm( - `Confirm DNS settings? (Add Local Domain: ${domainip} ${domain})`, - ) - ) { - for (const [key, value] of Object.entries(settings.hosts)) { - if (key === domainip) { - if (Array.isArray(value)) { - if (!value.includes(domain)) { - value.push(domain); - updateLocalDomainsTable(); - return; - } - return; - } - const domainsarray = [value, domain]; - settings.hosts[key] = domainsarray; - updateLocalDomainsTable(); - return; - } + const domainip = prompt("Input localdomain IP:"); + if (domainip === null) return; + const domain = prompt("Input domain:"); + if (domainip === null || domain === null) return; + if ( + confirm(`Confirm DNS settings? (Add Local Domain: ${domainip} ${domain})`) + ) { + for (const [key, value] of Object.entries(settings.hosts)) { + if (key === domainip) { + if (Array.isArray(value)) { + if (!value.includes(domain)) { + value.push(domain); + updateLocalDomainsTable(); + return; + } + return; } - settings.hosts[domainip] = domain; + const domainsarray = [value, domain]; + settings.hosts[key] = domainsarray; updateLocalDomainsTable(); + return; + } } + settings.hosts[domainip] = domain; + updateLocalDomainsTable(); + } }); local_domains_table_body.addEventListener("click", (e) => { - if (e.target.classList.contains("deldomain")) { - if (confirm(`Confirm DNS settings? (delete domain: ${e.target.id})`)) { - const domainip = e.target.id.split("|")[0]; - const domain = e.target.id.split("|")[1]; - for (const [key, value] of Object.entries(settings.hosts)) { - if (key === domainip) { - if (Array.isArray(value)) { - if (value.includes(domain)) { - value.splice(value.indexOf(domain), 1); - updateLocalDomainsTable(); - return; - } - return; - } - delete settings.hosts[key]; - updateLocalDomainsTable(); - return; - } + if (e.target.classList.contains("deldomain")) { + if (confirm(`Confirm DNS settings? (delete domain: ${e.target.id})`)) { + const domainip = e.target.id.split("|")[0]; + const domain = e.target.id.split("|")[1]; + for (const [key, value] of Object.entries(settings.hosts)) { + if (key === domainip) { + if (Array.isArray(value)) { + if (value.includes(domain)) { + value.splice(value.indexOf(domain), 1); + updateLocalDomainsTable(); + return; } + return; + } + delete settings.hosts[key]; + updateLocalDomainsTable(); + return; } + } } + } }); const getConfigFile = () => { - fetch(window.location.origin + "/api/settings/file") - .then((data) => data.text()) - .then((textdata) => (edit_file_input.value = textdata)) - .catch((e) => alert(e)); + fetch(window.location.origin + "/api/settings/file") + .then((data) => data.text()) + .then((textdata) => (edit_file_input.value = textdata)) + .catch((e) => alert(e)); }; getConfigFile(); -save_config_file_button.addEventListener("click", () => { - if (confirm("Save Config file?")) { - fetch(window.location.origin + "/api/settings/file", { - method: "POST", - body: edit_file_input.value, - }) - .then((data) => data.text()) - .then((textdata) => { - edit_file_input.value = textdata; - alert("Success write settings file"); - }) - .catch((e) => alert(e)); +save_config_file_button.addEventListener("click", async () => { + if (confirm("Save Config file?")) { + var response = await fetch(window.location.origin + "/api/settings/file", { + method: "POST", + body: edit_file_input.value, + }); + if (!response.ok) { + const result = await response.json(); + alert(result.error || "Failed to save config file"); + return; } + const result = await response.json(); + if (result.error) { + alert(result.error); + } else { + edit_file_input.value = result.data; + alert("Success write settings file"); + } + } }); From 293f77829aee492731e45aabf04eff5d72811340 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Tue, 9 Sep 2025 14:03:52 +0700 Subject: [PATCH 3/5] Update file.c --- src/lib/file.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/file.c b/src/lib/file.c index eb9d28b..c7e355a 100644 --- a/src/lib/file.c +++ b/src/lib/file.c @@ -43,7 +43,6 @@ char *write_file(const char *path, const char *data) { perror("Error opening file"); return nullptr; } - printf("%s\n%s\n", data, path); if (fprintf(fp, "%s", data) < 0) { perror("Error write file"); return nullptr; From 4e36d1a86e16a63337959725abe59a811567e397 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Wed, 10 Sep 2025 15:20:37 +0700 Subject: [PATCH 4/5] added set_settings functional --- meson.build | 4 +++ src/set_dnsmasq_settings_field.c | 44 ++++++++++++++++++++++++ src/set_hosts.c | 44 ++++++++++++++++++++++++ src/set_resolvs.c | 29 ++++++++++++++++ src/set_settings.c | 58 ++++++++++++++++++++++++++++++++ src/settings.h | 3 ++ web/js/settings/settings.js | 6 +++- 7 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/set_dnsmasq_settings_field.c create mode 100644 src/set_hosts.c create mode 100644 src/set_resolvs.c diff --git a/meson.build b/meson.build index 52db0cb..7830e17 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,10 @@ executable( 'src/set_settings_file.c', 'src/get_settings_file.c', 'src/static.c', + 'src/set_hosts.c', + 'src/set_resolvs.c', + 'src/set_dnsmasq_settings_field.c', + 'src/restart_dnsmasq.c', include_directories: include_dirs, dependencies: deps, ) diff --git a/src/set_dnsmasq_settings_field.c b/src/set_dnsmasq_settings_field.c new file mode 100644 index 0000000..48ce12f --- /dev/null +++ b/src/set_dnsmasq_settings_field.c @@ -0,0 +1,44 @@ +#include "config.h" +#include "settings.h" + +#define DNSMASQ_CONFIG_FILE_TMP "dnsmasq.conf.tmp" + +char *set_dnsmasq_settings_field(const char *key, cJSON *setting) { + char *filename = cm_get_parameter_as_string(config, "dnsmasq_config"); + if (!filename) { + return "Failed to read config file"; + } + FILE *dnsmasq_config_file = fopen(filename, "r"); + FILE *dnsmasq_config_file_tmp = fopen(DNSMASQ_CONFIG_FILE_TMP, "w"); + + if (dnsmasq_config_file == nullptr || dnsmasq_config_file_tmp == nullptr) { + return "Failed to open config file"; + } + + char line[256]; + char *new_str = malloc(1024); + snprintf(new_str, 1024, "%s=%d", key, setting->valueint); + + while (fgets(line, sizeof(line), dnsmasq_config_file)) { + if (strstr(line, key) != nullptr || strcmp(line, key) == 0) { + fputs(new_str, dnsmasq_config_file_tmp); + fputs("\n", dnsmasq_config_file_tmp); + } else { + fputs(line, dnsmasq_config_file_tmp); + } + } + + fclose(dnsmasq_config_file); + fclose(dnsmasq_config_file_tmp); + free(new_str); + + if (remove(filename) != 0) { + remove(DNSMASQ_CONFIG_FILE_TMP); + return "Failed to remove config file"; + } + if (rename(DNSMASQ_CONFIG_FILE_TMP, filename) != 0) { + remove(DNSMASQ_CONFIG_FILE_TMP); + return "Failed to rename config file"; + } + return nullptr; +} diff --git a/src/set_hosts.c b/src/set_hosts.c new file mode 100644 index 0000000..d78738a --- /dev/null +++ b/src/set_hosts.c @@ -0,0 +1,44 @@ +#include "settings.h" + +#define DNSMASQ_HOSTS_FILE_TMP "hosts.tmp" + +char *set_hosts(cJSON *hosts) { + FILE *hosts_file_tmp = fopen(DNSMASQ_HOSTS_FILE_TMP, "w"); + if (!hosts_file_tmp) { + return "can't open hosts file"; + } + + cJSON *host = hosts->child; + while (host) { + if (!cJSON_IsString(host) && !cJSON_IsArray(host)) { + remove(DNSMASQ_HOSTS_FILE_TMP); + return "Invalid hosts"; + } + if (cJSON_IsString(host)) { + fprintf(hosts_file_tmp, "%s %s\n", host->string, host->valuestring); + } + if (cJSON_IsArray(host)) { + int arrsize = cJSON_GetArraySize(host); + if (arrsize > 0) { + fprintf(hosts_file_tmp, "%s ", host->string); + } + for (int i = 0; i < arrsize; i++) { + cJSON *domain = cJSON_GetArrayItem(host, i); + if (domain && cJSON_IsString(domain)) { + fprintf(hosts_file_tmp, "%s ", domain->valuestring); + } + } + fprintf(hosts_file_tmp, "\n"); + } + host = host->next; + } + + fclose(hosts_file_tmp); + + if (rename(DNSMASQ_HOSTS_FILE_TMP, "/etc/dnsmasq.conf.d/hosts") != 0) { + remove(DNSMASQ_HOSTS_FILE_TMP); + return "Failed to rename hosts file"; + } + remove(DNSMASQ_HOSTS_FILE_TMP); + return nullptr; +} diff --git a/src/set_resolvs.c b/src/set_resolvs.c new file mode 100644 index 0000000..cf8e91d --- /dev/null +++ b/src/set_resolvs.c @@ -0,0 +1,29 @@ +#include "settings.h" + +#define DNSMASQ_RESOLV_FILE_TMP "resolv.conf.tmp" + +char *set_resolvs(cJSON *resolvs) { + FILE *resolvs_file_tmp = fopen(DNSMASQ_RESOLV_FILE_TMP, "w"); + if (!resolvs_file_tmp) { + return "Failed to open resolv file"; + } + + cJSON *resolv = resolvs->child; + while (resolv) { + if (!cJSON_IsString(resolv)) { + remove(DNSMASQ_RESOLV_FILE_TMP); + return "Invalid resolv"; + } + fprintf(resolvs_file_tmp, "nameserver %s\n", resolv->valuestring); + resolv = resolv->next; + } + + fclose(resolvs_file_tmp); + + if (rename(DNSMASQ_RESOLV_FILE_TMP, "/etc/dnsmasq.conf.d/resolv.conf") != 0) { + remove(DNSMASQ_RESOLV_FILE_TMP); + return "Failed to rename resolv file"; + } + remove(DNSMASQ_RESOLV_FILE_TMP); + return nullptr; +} diff --git a/src/set_settings.c b/src/set_settings.c index f898438..e2e2549 100644 --- a/src/set_settings.c +++ b/src/set_settings.c @@ -1,4 +1,62 @@ #include "settings.h" void set_settings(struct mg_connection *c, struct mg_http_message *hm) { + cJSON *jsonbody = cJSON_Parse(hm->body.buf); + if (!jsonbody) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("can't parse JSON body")); + return; + } + + cJSON *cache_size = cJSON_GetObjectItemCaseSensitive(jsonbody, "cache_size"); + if (!cache_size || !cJSON_IsNumber(cache_size)) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("invalid cache_size")); + cJSON_Delete(jsonbody); + return; + } + + char *result_set_dnsmasq_settings_field = + set_dnsmasq_settings_field("cache-size", cache_size); + if (result_set_dnsmasq_settings_field) { + mg_http_reply(c, 200, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC(result_set_dnsmasq_settings_field)); + cJSON_Delete(jsonbody); + return; + } + + cJSON *resolvs = cJSON_GetObjectItemCaseSensitive(jsonbody, "resolv"); + if (!resolvs || !cJSON_IsArray(resolvs)) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("invalid resolv")); + cJSON_Delete(jsonbody); + return; + } + + char *result_set_resolvs = set_resolvs(resolvs); + if (result_set_resolvs) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC(result_set_resolvs)); + cJSON_Delete(jsonbody); + return; + } + + cJSON *hosts = cJSON_GetObjectItemCaseSensitive(jsonbody, "hosts"); + if (!hosts || !cJSON_IsObject(hosts)) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC("invalid hosts")); + cJSON_Delete(jsonbody); + return; + } + + char *result_set_hosts = set_hosts(hosts); + if (result_set_hosts) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC(result_set_hosts)); + cJSON_Delete(jsonbody); + return; + } + + mg_http_reply(c, 200, HTTP_HEADERS, "%s", hm->body.buf); + cJSON_Delete(jsonbody); } diff --git a/src/settings.h b/src/settings.h index 99038a4..50f4624 100644 --- a/src/settings.h +++ b/src/settings.h @@ -11,3 +11,6 @@ void set_settings(struct mg_connection *c, struct mg_http_message *hm); cJSON *decode_dnsmasq_settings(char *input, char *pair_delim, char *kv_delim); char **decode_resolvs(char *input, int *count); cJSON *decode_hosts(const char *input); +char *set_hosts(cJSON* hosts); +char *set_resolvs(cJSON* resolvs); +char *set_dnsmasq_settings_field(const char *key, cJSON *setting); diff --git a/web/js/settings/settings.js b/web/js/settings/settings.js index a8ba708..3a24ff3 100644 --- a/web/js/settings/settings.js +++ b/web/js/settings/settings.js @@ -76,6 +76,10 @@ const setSettings = () => { }) .then((data) => data.json()) .then((jsondata) => { + if (jsondata.error) { + alert(jsondata.error); + return; + } settings = jsondata; updateInterface(); alert("Success set settings"); @@ -88,7 +92,7 @@ save_settings_button.addEventListener("click", () => { }); const write_cache_size = (value) => { - settings.cache_size = value; + settings.cache_size = parseInt(value); }; cache_size_input.addEventListener("change", (e) => { From b65e3d2c8c3b3cb8550bd79996c6c6854918c66e Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Wed, 10 Sep 2025 15:33:37 +0700 Subject: [PATCH 5/5] added autorestart dnsmasq and updated README.md --- README.md | 106 ++++++++++++++++++++++++++++++++++++++------- src/set_settings.c | 9 ++++ 2 files changed, 100 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 818d5a8..04566ff 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,18 @@ The service consists of: - **Web Interface**: Bootstrap-based responsive UI - **Configuration Parser**: Custom dnsmasq config parser - **File Operations**: Safe file reading/writing utilities +- **Settings Management**: Direct manipulation of dnsmasq configuration files + - Updates dnsmasq main configuration (`/etc/dnsmasq.conf`) + - Manages hosts file (`/etc/dnsmasq.conf.d/hosts`) + - Manages resolv.conf (`/etc/dnsmasq.conf.d/resolv.conf`) + - Automatically restarts dnsmasq service after configuration changes ## API Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/api/settings` | Get current DNS settings | -| POST | `/api/settings` | Update DNS settings | +| POST | `/api/settings` | Update DNS settings (cache_size, resolv servers, hosts) and restart dnsmasq | | GET | `/api/settings/file` | Get raw configuration file | | POST | `/api/settings/file` | Update configuration file | | GET | `/#` | Serve web interface | @@ -43,6 +48,8 @@ The service consists of: ### System Dependencies - dnsmasq (for DNS service functionality) - SSL certificates (for HTTPS support) +- System initialization service (systemd, openrc, rc, finit, or init) +- Appropriate privileges to restart system services ## Installation @@ -76,7 +83,7 @@ Edit `config.json` to configure the service: "tls_enabled": true, "tls_cert": "/path/to/certificate.crt", "tls_key": "/path/to/private.key", - "system_initialization": "rc", + "system_initialization": "systemd", "dnsmasq_config": "/etc/dnsmasq.conf" } ``` @@ -89,7 +96,11 @@ Edit `config.json` to configure the service: - `tls_enabled`: Enable/disable HTTPS support - `tls_cert`: Path to SSL certificate file - `tls_key`: Path to SSL private key file -- `system_initialization`: System initialization type +- `system_initialization`: System initialization type for restarting dnsmasq service + - `"systemd"`: Uses `systemctl restart dnsmasq` + - `"rc"` or `"openrc"`: Uses `rc restart dnsmasq` + - `"finit"`: Uses `initctl restart dnsmasq` + - `"init"`: Uses `/etc/init.d/dnsmasq restart` - `dnsmasq_config`: Path to dnsmasq configuration file ## Usage @@ -105,11 +116,13 @@ The service will start HTTP server on the configured address (default: `0.0.0.0: Navigate to `http://localhost:8321` (or your configured address) to access the web interface where you can: -- Configure upstream DNS servers -- Manage local domain mappings +- Configure upstream DNS servers (resolv) +- Manage local domain mappings (hosts) - Enable/disable DHCP with range settings - Adjust DNS cache size - Edit configuration files directly +- Save settings directly to dnsmasq configuration +- Automatic dnsmasq service restart after configuration changes ### API Usage @@ -122,22 +135,63 @@ curl -X GET http://localhost:8321/api/settings ```bash curl -X POST http://localhost:8321/api/settings \ -H "Content-Type: application/json" \ - -d '{"cache_size": 1000, "resolv": ["8.8.8.8", "8.8.4.4"]}' + -d '{ + "cache_size": 1000, + "resolv": ["8.8.8.8", "8.8.4.4"], + "hosts": { + "192.168.1.1": "router.local", + "192.168.1.100": ["server1.local", "server1"] + } + }' ``` +The POST `/api/settings` endpoint accepts the following parameters: +- `cache_size` (integer): DNS cache size for dnsmasq +- `resolv` (array of strings): Upstream DNS servers +- `hosts` (object): Local DNS mappings where keys are IP addresses and values are either single hostnames (string) or arrays of hostnames + +**Note**: After successfully updating the configuration files, the service automatically restarts dnsmasq to apply the changes. The restart method depends on the `system_initialization` setting in `config.json`. + +#### Settings Configuration Details + +**Cache Size (`cache_size`)** +- Configures the DNS cache size in dnsmasq +- Updates the `cache-size` parameter in `/etc/dnsmasq.conf` +- Accepts integer values (recommended: 150-10000 depending on system resources) + +**Upstream DNS Servers (`resolv`)** +- Array of DNS server IP addresses to use for upstream resolution +- Creates/updates `/etc/dnsmasq.conf.d/resolv.conf` with `nameserver` entries +- Example: `["8.8.8.8", "1.1.1.1", "8.8.4.4"]` + +**Local Hosts (`hosts`)** +- Object mapping IP addresses to hostnames for local DNS resolution +- Creates/updates `/etc/dnsmasq.conf.d/hosts` file +- Supports both single hostnames and multiple hostnames per IP +- Format: + - Single hostname: `"192.168.1.1": "router.local"` + - Multiple hostnames: `"192.168.1.100": ["server1.local", "server1"]` + ## Development ### Project Structure ``` dns_service_c/ ├── src/ -│ ├── lib/ # Third-party libraries -│ │ ├── mongoose.c # HTTP server -│ │ ├── cJSON.c # JSON parser +│ ├── lib/ # Third-party libraries +│ │ ├── mongoose.c # HTTP server +│ │ ├── cJSON.c # JSON parser │ │ └── ... -│ ├── main.c # Application entry point -│ ├── config.c # Configuration management -│ ├── settings.c # DNS settings handlers +│ ├── main.c # Application entry point +│ ├── config.c # Configuration management +│ ├── settings.c # DNS settings handlers (GET) +│ ├── set_settings.c # DNS settings handlers (POST) +│ ├── set_dnsmasq_settings_field.c # Update dnsmasq config fields +│ ├── set_hosts.c # Manage hosts file +│ ├── set_resolvs.c # Manage resolv.conf +│ ├── restart_dnsmasq.c # Restart dnsmasq service +│ ├── settings.h # Settings function declarations +│ ├── restart_dnsmasq.h # Restart function declarations │ └── ... ├── web/ # Web interface files │ ├── index.html @@ -164,10 +218,15 @@ clang-format -i src/*.c src/*.h ## Security Considerations -- The service runs with system privileges to modify DNS configuration +- The service runs with system privileges to modify DNS configuration files - HTTPS is strongly recommended for production deployments - Ensure proper file permissions on certificate files +- The service directly modifies system files (`/etc/dnsmasq.conf`, `/etc/dnsmasq.conf.d/hosts`, `/etc/dnsmasq.conf.d/resolv.conf`) - Consider firewall rules to restrict access to management interface +- Validate all input data to prevent configuration file corruption +- Backup configuration files before making changes +- Ensure the service has appropriate write permissions to dnsmasq configuration directories +- The service requires privileges to restart the dnsmasq service via system commands ## Troubleshooting @@ -176,17 +235,34 @@ clang-format -i src/*.c src/*.h 1. **Permission denied when accessing dnsmasq config** - Ensure the service has read/write permissions to the dnsmasq configuration file - Run with appropriate privileges or adjust file permissions + - Check permissions for `/etc/dnsmasq.conf.d/` directory -2. **SSL/TLS certificate errors** +3. **Failed to write settings** + - Verify write permissions to `/etc/dnsmasq.conf`, `/etc/dnsmasq.conf.d/hosts`, and `/etc/dnsmasq.conf.d/resolv.conf` + - Ensure the `/etc/dnsmasq.conf.d/` directory exists + - Check disk space availability + +4. **Failed to restart dnsmasq** + - Ensure the service has appropriate privileges to restart system services + - Verify the `system_initialization` parameter in `config.json` matches your system + - Supported initialization systems: rc, openrc, systemd, finit, init + - Check that dnsmasq service is properly installed and configured on the system + +5. **SSL/TLS certificate errors** - Verify certificate and key file paths in `config.json` - Ensure certificate files have correct permissions - Check certificate validity and format -3. **Port binding errors** +6. **Port binding errors** - Verify ports are not already in use - Check firewall settings - Ensure proper privileges for binding to privileged ports +7. **Settings not persisting after dnsmasq restart** + - Ensure dnsmasq is configured to read from `/etc/dnsmasq.conf.d/` directory + - Verify the main dnsmasq configuration includes the conf-dir directive + - Check that temporary files are properly cleaned up after configuration updates + ### Logging The service uses Mongoose's built-in logging. Set log level in code or use debug build for verbose output. diff --git a/src/set_settings.c b/src/set_settings.c index e2e2549..22c52db 100644 --- a/src/set_settings.c +++ b/src/set_settings.c @@ -1,4 +1,5 @@ #include "settings.h" +#include "restart_dnsmasq.h" void set_settings(struct mg_connection *c, struct mg_http_message *hm) { cJSON *jsonbody = cJSON_Parse(hm->body.buf); @@ -57,6 +58,14 @@ void set_settings(struct mg_connection *c, struct mg_http_message *hm) { return; } + char *error = restart_dnsmasq(); + if (error) { + mg_http_reply(c, 500, HTTP_HEADERS, "{%m:%m}", MG_ESC("error"), + MG_ESC(error)); + free(error); + return; + } + mg_http_reply(c, 200, HTTP_HEADERS, "%s", hm->body.buf); cJSON_Delete(jsonbody); }