Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c739bd71c9 | ||
|
|
7b8721414a | ||
|
|
14045198a0 | ||
|
|
f01755caa8 | ||
|
|
49f6415c30 | ||
|
|
42d17285b5 | ||
|
|
8e05c41169 | ||
|
|
3418307e84 | ||
|
|
d1859af112 | ||
|
|
efcb19aa2f | ||
|
|
a4d0bfe9a7 | ||
|
|
952587df4f | ||
|
|
af8395aeb1 | ||
|
|
d40b764b17 | ||
|
|
e8f1e78664 | ||
|
|
fc595d68c7 | ||
|
|
5202a0e871 | ||
|
|
13fb3d859a | ||
|
|
00424a90f5 | ||
|
|
fb3cd0636e |
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user