20 Commits
Author SHA1 Message Date
wt c739bd71c9 Merge pull request 'added finit init system support' (#8) from develop into main
Build / Build (push) Successful in 12s
Reviewed-on: https://gitea.home.com:3000/TolaMironcenko/dns_service/pulls/8
2025-08-16 22:49:18 +07:00
wt 7b8721414a added finit init system support
Build / Build (push) Successful in 19s
2025-08-16 22:38:50 +07:00
wt 14045198a0 Merge pull request 'some refactoring' (#7) from refactor into main
Build / Build (push) Successful in 9s
Reviewed-on: https://gitea.home.com:3000/TolaMironcenko/dns_service/pulls/7
2025-07-16 14:46:19 +07:00
wt f01755caa8 some refactoring
Build / Build (push) Successful in 10s
2025-07-16 14:45:52 +07:00
wt 49f6415c30 Merge pull request 'updated README.md and added LICENSE' (#6) from docs-and-license into main
Build / Build (push) Successful in 9s
Reviewed-on: https://gitea.home.com:3000/TolaMironcenko/dns_service/pulls/6
2025-07-16 14:38:27 +07:00
wt 42d17285b5 updated README.md and added LICENSE
Build / Build (push) Successful in 20s
2025-07-16 14:37:09 +07:00
wt 8e05c41169 Merge pull request 'Update settings.js' (#5) from alerts into main
Build / Build (push) Successful in 9s
Reviewed-on: https://gitea.home.com:3000/TolaMironcenko/dns_service/pulls/5
2025-07-09 21:01:46 +07:00
wt 3418307e84 Update settings.js
Build / Build (push) Successful in 12s
2025-07-09 21:00:42 +07:00
wt d1859af112 Merge pull request 'more optimizations' (#4) from actions into main
Build / Build (push) Successful in 30s
Reviewed-on: https://gitea.home.com:3000/TolaMironcenko/dns_service/pulls/4
2025-07-07 22:41:15 +07:00
wt efcb19aa2f more optimizations
Build / Build (push) Successful in 32s
2025-07-07 22:40:06 +07:00
wt a4d0bfe9a7 Merge pull request 'actions' (#3) from actions into main
Build / Build (push) Successful in 9s
Reviewed-on: https://gitea.home.com:3000/TolaMironcenko/dns_service/pulls/3
2025-07-07 21:35:35 +07:00
wt 952587df4f Update actions.yaml
Build / Build (push) Successful in 10s
2025-07-07 21:33:15 +07:00
wt af8395aeb1 Update actions.yaml 2025-07-07 21:33:08 +07:00
wt d40b764b17 Update actions.yaml
Build / Build (push) Failing after 41s
2025-07-07 21:27:43 +07:00
wt e8f1e78664 Update actions.yaml
Build / Build (push) Failing after 14s
2025-07-07 21:26:03 +07:00
wt fc595d68c7 Update actions.yaml
Build / Build (push) Failing after 37s
2025-07-07 21:24:37 +07:00
wt 5202a0e871 Update actions.yaml
Build / Build (push) Failing after 47s
2025-07-07 21:23:37 +07:00
wt 13fb3d859a Update actions.yaml
Build / Build (push) Failing after 1m6s
2025-07-07 21:19:12 +07:00
wt 00424a90f5 Update actions.yaml
Build / Build (push) Failing after 2s
2025-07-07 21:17:24 +07:00
wt fb3cd0636e Create actions.yaml
Build / Build (push) Failing after 3s
2025-07-07 21:13:16 +07:00
26 changed files with 1005 additions and 443 deletions
+14
View File
@@ -0,0 +1,14 @@
name: Build
run-name: ${{ gitea.actor }}
on: [push]
jobs:
Build:
runs-on: golang-1.24.4-alpine
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Prepare build
run: go mod download && go mod tidy
- name: Build
run: go build
+21
View File
@@ -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.
+430 -48
View File
@@ -1,83 +1,465 @@
# DNS_SERVICE (frontend for dnsmasq)
# DNS Service
- [routes](#routes)
- [build](#build)
- [configure](#configure)
- [example](#example)
- [requests responses](#requests-responses)
- [/api/settings](#settings)
- [/api/settings/file](#settingsfile)
A Go-based web frontend for managing dnsmasq DNS server configurations. This service provides a RESTful API and web interface for dynamically managing DNS settings, host mappings, and resolver configurations. The application features a modular architecture with separate packages for server management, API handling, and utility functions.
## routes
## Features
- `GET` `/` - frontend
- `GET` `/api/settings` - get all settings
- `POST` `/api/settings` - set all settings
- `GET` `/api/settings/file` - get settings file content
- `POST` `/api/settings/file` - set settings file content
- **Web Interface**: Clean, responsive frontend for managing DNS settings
- **RESTful API**: JSON-based API for programmatic configuration management
- **Dynamic Configuration**: Real-time updates to dnsmasq configuration
- **Host Management**: Easy management of hostname-to-IP mappings
- **Resolver Configuration**: Flexible DNS resolver settings
- **TLS Support**: Optional HTTPS/TLS encryption
- **Service Integration**: Systemd-compatible service management
- **Cache Control**: Configurable DNS cache size settings
- **Modular Architecture**: Clean separation of concerns with dedicated packages
- **Error Handling**: Comprehensive error handling and logging
## build
## Prerequisites
```shell
go build
- Go 1.24.4 or higher
- dnsmasq installed and configured
- Linux system with systemd (for service management)
- SSL certificates (for TLS mode)
## Installation
### From Source
1. Clone the repository:
```bash
git clone <repository-url>
cd dns_service
```
## configure
### example
2. Build the application:
```bash
go build -o dns_service
```
3. Install the binary:
```bash
sudo cp dns_service /usr/bin/
sudo chmod +x /usr/bin/dns_service
```
4. Create configuration directory:
```bash
sudo mkdir -p /etc/dns_service
```
5. Copy configuration files:
```bash
sudo cp dns_service.conf /etc/dns_service/
sudo cp dns_service.service /etc/init.d/
sudo chmod +x /etc/init.d/dns_service.service
```
## Configuration
### Environment Variables
The service is configured via environment variables in `/etc/dns_service/dns_service.conf`:
```bash
# dnsmasq configuration file path
DNSMASQ_CONFIG=/etc/dnsmasq.conf
# TLS configuration
ENABLE_TLS=true
CERT_FILE=/etc/dns_service/dns.home.com.crt
KEY_FILE=/etc/dns_service/dns.home.com.key
# Server configuration
SERVER_ADDRESS=0.0.0.0
SERVER_PORT=8090
# System initialization type
SYSTEM_INITIALIZATION=rc
```
## requests-responses
### Configuration Parameters
### settings
| Parameter | Description | Default | Required |
|-----------|-------------|---------|----------|
| `DNSMASQ_CONFIG` | Path to dnsmasq configuration file | `/etc/dnsmasq.conf` | Yes |
| `ENABLE_TLS` | Enable HTTPS/TLS encryption | `false` | No |
| `CERT_FILE` | Path to SSL certificate file | - | If TLS enabled |
| `KEY_FILE` | Path to SSL private key file | - | If TLS enabled |
| `SERVER_ADDRESS` | Server bind address | `0.0.0.0` | No |
| `SERVER_PORT` | Server port | `8090` | No |
| `SYSTEM_INITIALIZATION` | Init system type | `rc` | No |
#### `/api/settings`
### TLS Setup
For TLS mode, ensure you have valid SSL certificates:
```bash
# Generate self-signed certificate (for testing)
sudo openssl req -x509 -newkey rsa:4096 -keyout /etc/dns_service/dns.home.com.key \
-out /etc/dns_service/dns.home.com.crt -days 365 -nodes
# Set proper permissions
sudo chmod 600 /etc/dns_service/dns.home.com.key
sudo chmod 644 /etc/dns_service/dns.home.com.crt
```
## API Documentation
### Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/` | Web frontend interface |
| `GET` | `/api/settings` | Get current DNS settings |
| `POST` | `/api/settings` | Update DNS settings |
| `GET` | `/api/settings/file` | Get raw dnsmasq configuration |
| `POST` | `/api/settings/file` | Update raw dnsmasq configuration |
### API Examples
#### Get Current Settings
```bash
curl -X GET http://localhost:8090/api/settings
```
**Response:**
```json
{
"cache_size": 10000,
"hosts": {
"185.130.83.26": "gitea.home.com",
"192.168.3.1": "router.home.com",
"192.168.3.113": [
"samba.home.com",
"gitea.home.com",
"navidrome.home.com"
],
"192.168.3.119": [
"immich.home.com",
"jellyfin.home.com",
"portainer.home.com"
],
"192.168.3.155": "dns.home.com"
},
"resolv": [
"192.168.3.1",
"109.194.32.1",
"5.3.3.3",
"8.8.8.8"
]
"cache_size": 10000,
"hosts": {
"185.130.83.26": "gitea.home.com",
"192.168.3.1": "router.home.com",
"192.168.3.113": [
"samba.home.com",
"gitea.home.com",
"navidrome.home.com"
],
"192.168.3.119": [
"immich.home.com",
"jellyfin.home.com",
"portainer.home.com"
],
"192.168.3.155": "dns.home.com"
},
"resolv": [
"192.168.3.1",
"109.194.32.1",
"5.3.3.3",
"8.8.8.8"
]
}
```
### settingsfile
#### Update Settings
#### `/api/settings/file`
```bash
curl -X POST -H "Content-Type: application/json" \
-d '{"cache_size": 15000, "hosts": {"192.168.1.100": "server.local"}, "resolv": ["8.8.8.8", "8.8.4.4"]}' \
http://localhost:8090/api/settings
```
#### Get Configuration File
```bash
curl -X GET http://localhost:8090/api/settings/file
```
**Response:**
```
resolv-file=/etc/dnsmasq.conf.d/resolv.conf
addn-hosts=/etc/dnsmasq.conf.d/hosts
cache-size=10000
```
#### Update Configuration File
```bash
curl -X POST -H "Content-Type: text/plain" \
-d "resolv-file=/etc/dnsmasq.conf.d/resolv.conf
addn-hosts=/etc/dnsmasq.conf.d/hosts
cache-size=15000" \
http://localhost:8090/api/settings/file
```
## Service Management
### Start the Service
```bash
# Manual start
sudo /etc/init.d/dns_service.service start
# Or run directly
sudo /usr/bin/dns_service
```
### Stop the Service
```bash
sudo /etc/init.d/dns_service.service stop
```
### Restart the Service
```bash
sudo /etc/init.d/dns_service.service restart
```
### Check Service Status
```bash
# Check if process is running
ps aux | grep dns_service
# Check process ID
cat /var/run/dns_service.pid
```
## Development
### Project Structure
```
dns_service/
├── api/ # API handlers and frontend
│ ├── frontend/ # Web frontend assets
│ ├── settings.go # Settings API handler
│ ├── settings_file.go # Configuration file handler
│ └── spa.go # Single-page application handler
├── server/ # Server configuration and startup
│ ├── Configure.go # HTTP routes configuration
│ └── Run.go # Server startup and TLS handling
├── settings/ # Settings management
│ ├── get_settings.go # Get settings functionality
│ └── set_settings.go # Set settings functionality
├── utils/ # Utility functions
│ ├── LoadEnd.go # Environment loading utilities
│ ├── decode_hosts.go # Host parsing utilities
│ ├── decode_resolvs.go # Resolver parsing utilities
│ ├── decode_settings.go # Settings parsing utilities
│ ├── response_json.go # JSON response utilities
│ └── restart_dnsmasq.go # dnsmasq restart functionality
├── main.go # Application entry point
├── go.mod # Go module definition
├── go.sum # Go module checksums
├── dns_service.conf # Configuration file
├── dns_service.service # Init script
├── LICENSE # MIT License
└── README.md # This file
```
### Building
```bash
# Build for current platform
go build -o dns_service
# Build for Linux (cross-compilation)
GOOS=linux GOARCH=amd64 go build -o dns_service
# Build with version information
go build -ldflags "-X main.version=1.0.0" -o dns_service
```
### Testing
```bash
# Run tests
go test ./...
# Test with coverage
go test -cover ./...
# Test specific package
go test ./api
```
### Dependencies
The project uses minimal external dependencies:
- `github.com/joho/godotenv` - Environment variable loading from configuration files
### Architecture
The application follows a clean modular architecture with clear separation of concerns:
- **main.go**: Application entry point that orchestrates startup sequence
- **server/**: Server configuration and HTTP handling
- `Configure.go`: HTTP routes and middleware setup
- `Run.go`: Server startup with TLS/HTTP support and error handling
- **api/**: REST API endpoints and frontend serving
- `settings.go`: DNS settings API endpoints
- `settings_file.go`: Configuration file management endpoints
- `spa.go`: Single-page application serving
- **utils/**: Shared utility functions and environment management
- `LoadEnd.go`: Environment variable loading and validation
- Various decode utilities for parsing DNS configurations
- **settings/**: DNS settings management and persistence
- Configuration reading and writing functionality
### Modular Development Approach
The codebase is designed with modularity in mind:
**Benefits:**
- **Maintainability**: Each package has a single responsibility
- **Testability**: Individual components can be tested in isolation
- **Reusability**: Utility functions can be shared across packages
- **Scalability**: New features can be added without affecting existing code
- **Code Organization**: Clear structure makes onboarding easier
**Development Workflow:**
1. **Environment Setup**: `utils.LoadEnv()` loads configuration
2. **Server Configuration**: `server.Configure()` sets up routes
3. **Server Startup**: `server.Run()` starts the HTTP/HTTPS server
4. **API Handling**: Individual handlers process requests
5. **Settings Management**: Dedicated package manages DNS configurations
**Adding New Features:**
- Add new API endpoints in `api/` package
- Implement business logic in appropriate packages
- Add utility functions in `utils/` package
- Update server configuration if needed
### Code Examples
**Basic Server Setup:**
```go
package main
import (
"dns_service/server"
"dns_service/utils"
)
func main() {
utils.LoadEnv() // Load environment variables
server.Configure() // Configure HTTP routes
server.Run() // Start the server
}
```
**Adding Custom Middleware:**
```go
// In server/Configure.go
func Configure() {
// Add custom middleware
http.Handle("/", middleware(api.SpaHandler(http.FS(api.Frontend))))
http.HandleFunc("/api/settings", api.Settings)
http.HandleFunc("/api/settings/file", api.SettingsFile)
}
```
## Usage Examples
### Basic DNS Setup
1. Configure your local domains in the web interface
2. Set upstream DNS servers (Google DNS, Cloudflare, etc.)
3. Configure cache size based on your needs
4. Apply settings and restart dnsmasq
### Home Network Setup
Perfect for home labs and local development environments:
```json
{
"cache_size": 10000,
"hosts": {
"192.168.1.100": ["homelab.local", "api.homelab.local"],
"192.168.1.101": "nas.local",
"192.168.1.102": "router.local"
},
"resolv": ["192.168.1.1", "8.8.8.8", "1.1.1.1"]
}
```
### Corporate/Enterprise Network Setup
```json
{
"cache_size": 50000,
"hosts": {
"10.0.1.100": ["intranet.corp.local", "wiki.corp.local"],
"10.0.1.101": "mail.corp.local",
"10.0.1.102": "files.corp.local"
},
"resolv": ["10.0.1.1", "8.8.8.8"]
}
```
## Security Considerations
- **Always use TLS in production environments**
- **Restrict access to the management interface** (firewall rules, VPN, etc.)
- **Use strong SSL certificates from a trusted CA** (avoid self-signed in production)
- **Regularly update the service and dependencies**
- **Monitor logs for suspicious activity**
- **Consider firewall rules to limit access** to port 8090
- **Use strong file permissions** on configuration files
- **Regular security audits** of the system configuration
## Troubleshooting
### Common Issues
1. **Service won't start**:
- Check configuration file syntax and permissions
- Verify `/etc/dns_service/dns_service.conf` exists and is readable
- Check if required directories exist
2. **TLS errors**:
- Verify certificate files exist and have correct permissions
- Check certificate validity dates
- Ensure private key matches certificate
3. **Port conflicts**:
- Ensure port 8090 is not in use by another service
- Use `netstat -tlnp | grep 8090` to check port usage
4. **dnsmasq integration**:
- Verify dnsmasq is installed and properly configured
- Check if dnsmasq service is running
- Verify dnsmasq configuration file paths
### Logs
Check system logs for error messages:
```bash
# Check service logs
journalctl -u dns_service
# Check dnsmasq logs
journalctl -u dnsmasq
# Check application logs
tail -f /var/log/dns_service.log
```
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Support
For support and questions:
- Create an issue in the repository
- Check the documentation
- Review existing issues and discussions
---
**Note**: This service is designed to work with dnsmasq. Ensure dnsmasq is properly installed and configured before using this frontend.
+207
View File
@@ -0,0 +1,207 @@
const upstream_dns_servers_table_body = document.querySelector(
"#upstream-dns-servers-table-body",
);
const local_domains_table_body = document.querySelector(
"#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",
);
const edit_file_input = document.querySelector("#edit_file_input");
const save_config_file_button = document.querySelector(
"#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 += `<tr>
<th class="d-flex align-items-center justify-content-between">
${settings.resolv[i]}
<button id="${i}" class="delete-table-button btn btn-danger">Del</button>
</th>
</tr>`;
}
};
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 += `<tr>
<th>${key}</th>
<th>${value[i]}</th>
<th><button class="deldomain btn btn-danger" id="${key}|${value[i]}">Del</button>
</th>
</tr>`;
}
} else {
local_domains_table_body.innerHTML += `<tr>
<th>${key}</th>
<th>${value}</th>
<th><button class="deldomain btn btn-danger" id="${key}|${value}">Del</button>
</th>
</tr>`;
}
}
};
const updateInterface = () => {
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));
};
const setSettings = () => {
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");
})
.catch((e) => alert(e));
};
save_settings_button.addEventListener("click", () => {
setSettings();
});
const write_cache_size = (value) => {
settings.cache_size = value;
};
cache_size_input.addEventListener("change", (e) => {
write_cache_size(e.target.value);
});
cache_size_input.addEventListener("input", (e) => {
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();
}
}
}
});
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();
}
}
}
});
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;
}
}
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;
}
}
}
}
});
const getConfigFile = () => {
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));
}
});
+35
View File
@@ -0,0 +1,35 @@
package api
import (
"dns_service/settings"
"dns_service/utils"
"encoding/json"
"net/http"
)
func Settings(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case "GET":
respSettings, err := settings.GetSettings()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
utils.RespondWithJSON(w, respSettings, http.StatusOK)
case "POST":
var newSettings map[string]any
if err := json.NewDecoder(req.Body).Decode(&newSettings); err != nil {
http.Error(w, "invalid JSON format", http.StatusBadRequest)
return
}
respSettings, err := settings.SetSettings(newSettings)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
utils.RespondWithJSON(w, respSettings, http.StatusOK)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
+45
View File
@@ -0,0 +1,45 @@
package api
import (
"dns_service/utils"
"io"
"net/http"
"os"
)
func SettingsFile(w http.ResponseWriter, req *http.Request) {
configPath := os.Getenv("DNSMASQ_CONFIG")
if configPath == "" {
http.Error(w, "DNSMASQ_CONFIG environment variable not set", http.StatusInternalServerError)
return
}
switch req.Method {
case "GET":
data, err := os.ReadFile(configPath)
if err != nil {
http.Error(w, "failed to read dnsmasq config file", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write(data)
case "POST":
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, "failed to read request body", http.StatusBadRequest)
return
}
if err := os.WriteFile(configPath, bodyBytes, 0644); err != nil {
http.Error(w, "failed to write dnsmasq config file", http.StatusInternalServerError)
return
}
if err := utils.RestartDnsmasq(); err != nil {
http.Error(w, "failed to restart dnsmasq", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write(bodyBytes)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
+23
View File
@@ -0,0 +1,23 @@
package api
import (
"embed"
"fmt"
"net/http"
"path"
)
//go:embed frontend/*
var Frontend embed.FS
func SpaHandler(fs http.FileSystem) http.Handler {
fileServer := http.FileServer(fs)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("/frontend" + r.URL.Path)
r.URL.Path = "/frontend" + path.Clean(r.URL.Path)
if _, err := fs.Open(path.Clean(r.URL.Path)); err != nil {
r.URL.Path = "/"
}
fileServer.ServeHTTP(w, r)
})
}
-183
View File
@@ -1,183 +0,0 @@
const upstream_dns_servers_table_body = document.querySelector('#upstream-dns-servers-table-body')
const local_domains_table_body = document.querySelector('#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')
const edit_file_input = document.querySelector('#edit_file_input')
const save_config_file_button = document.querySelector('#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 += `<tr>
<th class="d-flex align-items-center justify-content-between">
${settings.resolv[i]}
<button id="${i}" class="delete-table-button btn btn-danger">Del</button>
</th>
</tr>`
}
}
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 += `<tr>
<th>${key}</th>
<th>${value[i]}</th>
<th><button class="deldomain btn btn-danger" id="${key}|${value[i]}">Del</button>
</th>
</tr>`
}
} else {
local_domains_table_body.innerHTML += `<tr>
<th>${key}</th>
<th>${value}</th>
<th><button class="deldomain btn btn-danger" id="${key}|${value}">Del</button>
</th>
</tr>`
}
}
}
const updateInterface = () => {
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()
})
}
const setSettings = () => {
fetch(window.location.origin + "/api/settings", {
method: 'POST',
body: JSON.stringify(settings)
}).then(data => data.json())
.then(jsondata => {
settings = jsondata
updateInterface()
})
}
save_settings_button.addEventListener('click', () => {
setSettings()
})
const write_cache_size = (value) => {
settings.cache_size = value
}
cache_size_input.addEventListener('change', (e) => {
write_cache_size(e.target.value)
})
cache_size_input.addEventListener('input', (e) => {
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()
}
}
}
})
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()
}
}
}
})
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
}
}
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
}
}
}
}
})
const getConfigFile = () => {
fetch(window.location.origin + "/api/settings/file")
.then(data => data.text())
.then(textdata => edit_file_input.value = textdata)
}
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)
}
})
+4 -115
View File
@@ -1,123 +1,12 @@
package main
import (
"dns_service/settings"
"dns_service/server"
"dns_service/utils"
"embed"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path"
"github.com/joho/godotenv"
)
func Settings(w http.ResponseWriter, req *http.Request) {
if req.Method == "GET" {
resp_settings, err := settings.GetSettings()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jsondata, err := json.Marshal(resp_settings)
if err != nil {
http.Error(w, "Error json marshal settings", http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, "%s", string(jsondata))
} else if req.Method == "POST" {
new_settings := make(map[string]any)
decoder := json.NewDecoder(req.Body)
decoder.Decode(&new_settings)
old_settings, err := settings.GetSettings()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp_settings, err := settings.SetSettings(new_settings, old_settings)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jsondata, err := json.Marshal(resp_settings)
if err != nil {
http.Error(w, "Error json marshal settings", http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
fmt.Println(new_settings, old_settings, resp_settings)
fmt.Fprintf(w, "%s", string(jsondata))
}
}
func SettingsFile(w http.ResponseWriter, req *http.Request) {
if req.Method == "GET" {
data, e := os.ReadFile(os.Getenv("DNSMASQ_CONFIG"))
if e != nil {
panic(e)
}
w.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(w, "%s", string(data))
} else if req.Method == "POST" {
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusBadRequest)
return
}
err = os.WriteFile(os.Getenv("DNSMASQ_CONFIG"), bodyBytes, 0644)
if err != nil {
http.Error(w, "Error writing dnsmasq config file", http.StatusInternalServerError)
return
}
fmt.Println(string(bodyBytes))
utils.RestartDnsmasq()
exec.Command("rc", "restart", "dnsmasq").Run()
w.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(w, "%s", string(bodyBytes))
}
}
//go:embed frontend/*
var frontend embed.FS
func spaHandler(fs http.FileSystem) http.Handler {
fileServer := http.FileServer(fs)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("/frontend" + r.URL.Path)
r.URL.Path = "/frontend" + path.Clean(r.URL.Path)
if _, err := fs.Open(path.Clean(r.URL.Path)); err != nil {
r.URL.Path = "/"
}
fileServer.ServeHTTP(w, r)
})
}
func main() {
err := godotenv.Load("/etc/dns_service/dns_service.conf")
if err != nil {
log.Fatal("Error loading .env file")
}
http.Handle("/", spaHandler(http.FS(frontend)))
http.HandleFunc("/api/settings", Settings)
http.HandleFunc("/api/settings/file", SettingsFile)
if os.Getenv("ENABLE_TLS") == "true" {
http.ListenAndServeTLS(
os.Getenv("SERVER_ADDRESS")+":"+os.Getenv("SERVER_PORT"),
os.Getenv("CERT_FILE"),
os.Getenv("KEY_FILE"),
nil,
)
} else {
http.ListenAndServe(
os.Getenv("SERVER_ADDRESS")+":"+os.Getenv("SERVER_PORT"),
nil,
)
}
utils.LoadEnv()
server.Configure()
server.Run()
}
+12
View File
@@ -0,0 +1,12 @@
package server
import (
"dns_service/api"
"net/http"
)
func Configure() {
http.Handle("/", api.SpaHandler(http.FS(api.Frontend)))
http.HandleFunc("/api/settings", api.Settings)
http.HandleFunc("/api/settings/file", api.SettingsFile)
}
+29
View File
@@ -0,0 +1,29 @@
package server
import (
"log"
"net/http"
"os"
)
func Run() {
if os.Getenv("ENABLE_TLS") == "true" {
err := http.ListenAndServeTLS(
os.Getenv("SERVER_ADDRESS")+":"+os.Getenv("SERVER_PORT"),
os.Getenv("CERT_FILE"),
os.Getenv("KEY_FILE"),
nil,
)
if err != nil {
log.Fatal(err)
}
} else {
err := http.ListenAndServe(
os.Getenv("SERVER_ADDRESS")+":"+os.Getenv("SERVER_PORT"),
nil,
)
if err != nil {
log.Fatal(err)
}
}
}
+36 -17
View File
@@ -2,32 +2,51 @@ package settings
import (
"dns_service/utils"
"fmt"
"os"
"strconv"
)
func GetSettings() (map[string]any, error) {
settings := make(map[string]any)
data, e := os.ReadFile(os.Getenv("DNSMASQ_CONFIG"))
if e != nil {
return settings, e
configPath := os.Getenv("DNSMASQ_CONFIG")
if configPath == "" {
return nil, fmt.Errorf("DNSMASQ_CONFIG environment variable not set")
}
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file at %s: %w", configPath, err)
}
dnsmasqSettings := utils.DecodeSettings(string(data), "\n", "=")
if dnsmasqSettings["cache-size"] != "" {
cache_size, _ := strconv.ParseInt(dnsmasqSettings["cache-size"], 10, 64)
settings["cache_size"] = cache_size
settings := make(map[string]any)
// Parse cache size
if cacheSizeStr, ok := dnsmasqSettings["cache-size"]; ok && cacheSizeStr != "" {
cacheSize, err := strconv.ParseInt(cacheSizeStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse cache-size '%s': %w", cacheSizeStr, err)
}
settings["cache_size"] = cacheSize
}
data, e = os.ReadFile(dnsmasqSettings["resolv-file"])
if e != nil {
return settings, e
// Read and parse resolv file
if resolvFile, ok := dnsmasqSettings["resolv-file"]; ok {
data, err := os.ReadFile(resolvFile)
if err != nil {
return nil, fmt.Errorf("failed to read resolv-file at %s: %w", resolvFile, err)
}
settings["resolv"] = utils.DecodeResolvs(string(data))
}
resolvSettings := utils.DecodeResolvs(string(data))
settings["resolv"] = resolvSettings
data, e = os.ReadFile(dnsmasqSettings["addn-hosts"])
if e != nil {
return settings, e
// Read and parse hosts file
if hostsFile, ok := dnsmasqSettings["addn-hosts"]; ok {
data, err := os.ReadFile(hostsFile)
if err != nil {
return nil, fmt.Errorf("failed to read addn-hosts file at %s: %w", hostsFile, err)
}
settings["hosts"] = utils.DecodeHosts(string(data))
}
hosts := utils.DecodeHosts(string(data))
settings["hosts"] = hosts
return settings, nil
}
+74 -39
View File
@@ -7,51 +7,86 @@ import (
"strings"
)
func SetSettings(new_settings map[string]any, old_settings map[string]any) (map[string]any, error) {
data, err := os.ReadFile(os.Getenv("DNSMASQ_CONFIG"))
// Helper to write file content
func writeFile(path, content string) error {
return os.WriteFile(path, []byte(content), 0644)
}
func SetSettings(new_settings map[string]any) (map[string]any, error) {
// 1. Get config path and read the main dnsmasq configuration
configPath := os.Getenv("DNSMASQ_CONFIG")
if configPath == "" {
return nil, fmt.Errorf("DNSMASQ_CONFIG environment variable not set")
}
data, err := os.ReadFile(configPath)
if err != nil {
return old_settings, err
return nil, fmt.Errorf("failed to read config file at %s: %w", configPath, err)
}
new_data := strings.Replace(
string(data),
"cache-size="+fmt.Sprintf("%v", old_settings["cache_size"]),
"cache-size="+fmt.Sprintf("%v", new_settings["cache_size"]),
1,
)
err = os.WriteFile(os.Getenv("DNSMASQ_CONFIG"), []byte(new_data), 0644)
if err != nil {
return old_settings, err
}
var resolvs string
new_resolvs := new_settings["resolv"].([]any)
for i := range new_resolvs {
resolvs += "nameserver " + new_resolvs[i].(string) + "\n"
}
var hosts string
new_hosts := new_settings["hosts"].(map[string]any)
for key, value := range new_hosts {
hosts += key
switch value.(type) {
case []any:
arrvalue := value.([]any)
for i := range arrvalue {
hosts += " " + arrvalue[i].(string)
dnsmasqSettings := utils.DecodeSettings(string(data), "\n", "=")
// 2. Update cache-size in the main config file
if newCacheSize, ok := new_settings["cache_size"]; ok {
oldCacheSize, oldOk := dnsmasqSettings["cache-size"]
// Replace the line if it exists, otherwise dnsmasq uses a default
if oldOk {
newConfigContent := strings.Replace(
string(data),
"cache-size="+oldCacheSize,
"cache-size="+fmt.Sprintf("%v", newCacheSize),
1,
)
if err := writeFile(configPath, newConfigContent); err != nil {
return nil, fmt.Errorf("failed to write updated config to %s: %w", configPath, err)
}
hosts += "\n"
break
case string:
hosts += " " + value.(string) + "\n"
}
}
fmt.Println(resolvs, hosts)
err = os.WriteFile("/etc/dnsmasq.conf.d/resolv.conf", []byte(resolvs), 0644)
if err != nil {
return old_settings, err
// 3. Update the resolv file
if newResolvs, ok := new_settings["resolv"].([]any); ok {
resolvFile, ok := dnsmasqSettings["resolv-file"]
if !ok {
return nil, fmt.Errorf("resolv-file path not found in dnsmasq config")
}
var b strings.Builder
for _, v := range newResolvs {
if s, ok := v.(string); ok {
fmt.Fprintf(&b, "nameserver %s\n", s)
}
}
if err := writeFile(resolvFile, b.String()); err != nil {
return nil, fmt.Errorf("failed to write to resolv file %s: %w", resolvFile, err)
}
}
err = os.WriteFile("/etc/dnsmasq.conf.d/hosts", []byte(hosts), 0644)
if err != nil {
return old_settings, err
// 4. Update the hosts file
if newHosts, ok := new_settings["hosts"].(map[string]any); ok {
hostsFile, ok := dnsmasqSettings["addn-hosts"]
if !ok {
return nil, fmt.Errorf("addn-hosts path not found in dnsmasq config")
}
var b strings.Builder
for key, value := range newHosts {
b.WriteString(key)
switch v := value.(type) {
case []any:
for _, ip := range v {
fmt.Fprintf(&b, " %s", ip)
}
b.WriteString("\n")
case string:
fmt.Fprintf(&b, " %s\n", v)
}
}
if err := writeFile(hostsFile, b.String()); err != nil {
return nil, fmt.Errorf("failed to write to hosts file %s: %w", hostsFile, err)
}
}
utils.RestartDnsmasq()
// 5. Restart dnsmasq to apply changes
if err := utils.RestartDnsmasq(); err != nil {
return nil, fmt.Errorf("failed to restart dnsmasq: %w", err)
}
return new_settings, nil
}
+14
View File
@@ -0,0 +1,14 @@
package utils
import (
"log"
"github.com/joho/godotenv"
)
func LoadEnv() {
err := godotenv.Load("/etc/dns_service/dns_service.conf")
if err != nil {
log.Fatal("Error loading .env file")
}
}
+12 -19
View File
@@ -4,28 +4,21 @@ import "strings"
func DecodeHosts(input string) map[string]any {
result := make(map[string]any)
pairs := strings.SplitSeq(input, "\n")
lines := strings.Split(input, "\n")
for pair := range pairs {
if pair == "" {
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
kv := strings.Split(pair, " ")
if len(kv) == 2 {
key := strings.TrimSpace(kv[0])
value := strings.TrimSpace(kv[1])
if key != "" {
result[key] = value
}
continue
}
if len(kv) != 0 {
key := strings.TrimSpace(kv[0])
var value []string
for i := 1; i < len(kv); i++ {
value = append(value, kv[i])
}
result[key] = value
key := fields[0]
if len(fields) == 1 {
result[key] = []string{}
} else if len(fields) == 2 {
result[key] = fields[1]
} else {
result[key] = fields[1:]
}
}
return result
+8 -12
View File
@@ -4,19 +4,15 @@ import "strings"
func DecodeResolvs(input string) []string {
var result []string
pairs := strings.SplitSeq(input, "\n")
lines := strings.Split(input, "\n")
for pair := range pairs {
if pair == "" {
continue
}
kv := strings.SplitN(pair, " ", 2)
if len(kv) != 2 {
continue
}
value := strings.TrimSpace(kv[1])
if value != "" {
result = append(result, value)
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "nameserver ") {
value := strings.TrimPrefix(trimmedLine, "nameserver ")
if value != "" {
result = append(result, value)
}
}
}
return result
+8 -6
View File
@@ -4,20 +4,22 @@ import "strings"
func DecodeSettings(input string, pairDelim string, kvDelim string) map[string]string {
result := make(map[string]string)
pairs := strings.SplitSeq(input, pairDelim)
pairs := strings.Split(input, pairDelim)
for pair := range pairs {
if pair == "" {
for _, pair := range pairs {
trimmedPair := strings.TrimSpace(pair)
if trimmedPair == "" {
continue
}
kv := strings.SplitN(pair, kvDelim, 2)
kv := strings.SplitN(trimmedPair, kvDelim, 2)
if len(kv) != 2 {
continue
}
key := strings.TrimSpace(kv[0])
value := strings.TrimSpace(kv[1])
if key != "" {
result[key] = value
result[key] = strings.TrimSpace(kv[1])
}
}
return result
+18
View File
@@ -0,0 +1,18 @@
package utils
import (
"encoding/json"
"net/http"
)
// Helper to write JSON responses
func RespondWithJSON(w http.ResponseWriter, data any, statusCode int) {
jsonData, err := json.Marshal(data)
if err != nil {
http.Error(w, "failed to marshal JSON", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
w.Write(jsonData)
}
+15 -4
View File
@@ -1,17 +1,28 @@
package utils
import (
"fmt"
"os"
"os/exec"
)
func RestartDnsmasq() {
func RestartDnsmasq() error {
var cmd *exec.Cmd
switch os.Getenv("SYSTEM_INITIALIZATION") {
case "rc":
exec.Command("rc", "restart", "dnsmasq").Run()
cmd = exec.Command("rc", "restart", "dnsmasq")
case "openrc":
exec.Command("rc-service", "dnsmasq", "restart").Run()
cmd = exec.Command("rc-service", "dnsmasq", "restart")
case "systemd":
exec.Command("systemctl", "restart", "dnsmasq").Run()
cmd = exec.Command("systemctl", "restart", "dnsmasq")
case "finit":
cmd = exec.Command("initctl", "restart", "dnsmasq")
default:
return fmt.Errorf("unsupported system initialization: %s", os.Getenv("SYSTEM_INITIALIZATION"))
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to restart dnsmasq: %w", err)
}
return nil
}