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 |
No files matched your search
@@ -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)
|
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.
|
||||||
- [build](#build)
|
|
||||||
- [configure](#configure)
|
|
||||||
- [example](#example)
|
|
||||||
- [requests responses](#requests-responses)
|
|
||||||
- [/api/settings](#settings)
|
|
||||||
- [/api/settings/file](#settingsfile)
|
|
||||||
|
|
||||||
## routes
|
## Features
|
||||||
|
|
||||||
- `GET` `/` - frontend
|
- **Web Interface**: Clean, responsive frontend for managing DNS settings
|
||||||
- `GET` `/api/settings` - get all settings
|
- **RESTful API**: JSON-based API for programmatic configuration management
|
||||||
- `POST` `/api/settings` - set all settings
|
- **Dynamic Configuration**: Real-time updates to dnsmasq configuration
|
||||||
- `GET` `/api/settings/file` - get settings file content
|
- **Host Management**: Easy management of hostname-to-IP mappings
|
||||||
- `POST` `/api/settings/file` - set settings file content
|
- **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 1.24.4 or higher
|
||||||
go build
|
- 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
|
2. Build the application:
|
||||||
|
```bash
|
||||||
### example
|
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
|
DNSMASQ_CONFIG=/etc/dnsmasq.conf
|
||||||
|
|
||||||
|
# TLS configuration
|
||||||
ENABLE_TLS=true
|
ENABLE_TLS=true
|
||||||
CERT_FILE=/etc/dns_service/dns.home.com.crt
|
CERT_FILE=/etc/dns_service/dns.home.com.crt
|
||||||
KEY_FILE=/etc/dns_service/dns.home.com.key
|
KEY_FILE=/etc/dns_service/dns.home.com.key
|
||||||
|
|
||||||
|
# Server configuration
|
||||||
SERVER_ADDRESS=0.0.0.0
|
SERVER_ADDRESS=0.0.0.0
|
||||||
SERVER_PORT=8090
|
SERVER_PORT=8090
|
||||||
|
|
||||||
|
# System initialization type
|
||||||
SYSTEM_INITIALIZATION=rc
|
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
|
```json
|
||||||
{
|
{
|
||||||
"cache_size": 10000,
|
"cache_size": 10000,
|
||||||
"hosts": {
|
"hosts": {
|
||||||
"185.130.83.26": "gitea.home.com",
|
"185.130.83.26": "gitea.home.com",
|
||||||
"192.168.3.1": "router.home.com",
|
"192.168.3.1": "router.home.com",
|
||||||
"192.168.3.113": [
|
"192.168.3.113": [
|
||||||
"samba.home.com",
|
"samba.home.com",
|
||||||
"gitea.home.com",
|
"gitea.home.com",
|
||||||
"navidrome.home.com"
|
"navidrome.home.com"
|
||||||
],
|
],
|
||||||
"192.168.3.119": [
|
"192.168.3.119": [
|
||||||
"immich.home.com",
|
"immich.home.com",
|
||||||
"jellyfin.home.com",
|
"jellyfin.home.com",
|
||||||
"portainer.home.com"
|
"portainer.home.com"
|
||||||
],
|
],
|
||||||
"192.168.3.155": "dns.home.com"
|
"192.168.3.155": "dns.home.com"
|
||||||
},
|
},
|
||||||
"resolv": [
|
"resolv": [
|
||||||
"192.168.3.1",
|
"192.168.3.1",
|
||||||
"109.194.32.1",
|
"109.194.32.1",
|
||||||
"5.3.3.3",
|
"5.3.3.3",
|
||||||
"8.8.8.8"
|
"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
|
resolv-file=/etc/dnsmasq.conf.d/resolv.conf
|
||||||
addn-hosts=/etc/dnsmasq.conf.d/hosts
|
addn-hosts=/etc/dnsmasq.conf.d/hosts
|
||||||
cache-size=10000
|
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.
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
File renamed without changes.
@@ -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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dns_service/settings"
|
"dns_service/server"
|
||||||
"dns_service/utils"
|
"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() {
|
func main() {
|
||||||
err := godotenv.Load("/etc/dns_service/dns_service.conf")
|
utils.LoadEnv()
|
||||||
if err != nil {
|
server.Configure()
|
||||||
log.Fatal("Error loading .env file")
|
server.Run()
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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 (
|
import (
|
||||||
"dns_service/utils"
|
"dns_service/utils"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetSettings() (map[string]any, error) {
|
func GetSettings() (map[string]any, error) {
|
||||||
settings := make(map[string]any)
|
configPath := os.Getenv("DNSMASQ_CONFIG")
|
||||||
data, e := os.ReadFile(os.Getenv("DNSMASQ_CONFIG"))
|
if configPath == "" {
|
||||||
if e != nil {
|
return nil, fmt.Errorf("DNSMASQ_CONFIG environment variable not set")
|
||||||
return settings, e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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", "=")
|
dnsmasqSettings := utils.DecodeSettings(string(data), "\n", "=")
|
||||||
if dnsmasqSettings["cache-size"] != "" {
|
settings := make(map[string]any)
|
||||||
cache_size, _ := strconv.ParseInt(dnsmasqSettings["cache-size"], 10, 64)
|
|
||||||
settings["cache_size"] = cache_size
|
// 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 {
|
// Read and parse resolv file
|
||||||
return settings, e
|
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
|
// Read and parse hosts file
|
||||||
data, e = os.ReadFile(dnsmasqSettings["addn-hosts"])
|
if hostsFile, ok := dnsmasqSettings["addn-hosts"]; ok {
|
||||||
if e != nil {
|
data, err := os.ReadFile(hostsFile)
|
||||||
return settings, e
|
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
|
return settings, nil
|
||||||
}
|
}
|
||||||
+74
-39
@@ -7,51 +7,86 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetSettings(new_settings map[string]any, old_settings map[string]any) (map[string]any, error) {
|
// Helper to write file content
|
||||||
data, err := os.ReadFile(os.Getenv("DNSMASQ_CONFIG"))
|
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 {
|
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(
|
dnsmasqSettings := utils.DecodeSettings(string(data), "\n", "=")
|
||||||
string(data),
|
|
||||||
"cache-size="+fmt.Sprintf("%v", old_settings["cache_size"]),
|
// 2. Update cache-size in the main config file
|
||||||
"cache-size="+fmt.Sprintf("%v", new_settings["cache_size"]),
|
if newCacheSize, ok := new_settings["cache_size"]; ok {
|
||||||
1,
|
oldCacheSize, oldOk := dnsmasqSettings["cache-size"]
|
||||||
)
|
// Replace the line if it exists, otherwise dnsmasq uses a default
|
||||||
err = os.WriteFile(os.Getenv("DNSMASQ_CONFIG"), []byte(new_data), 0644)
|
if oldOk {
|
||||||
if err != nil {
|
newConfigContent := strings.Replace(
|
||||||
return old_settings, err
|
string(data),
|
||||||
}
|
"cache-size="+oldCacheSize,
|
||||||
var resolvs string
|
"cache-size="+fmt.Sprintf("%v", newCacheSize),
|
||||||
new_resolvs := new_settings["resolv"].([]any)
|
1,
|
||||||
for i := range new_resolvs {
|
)
|
||||||
resolvs += "nameserver " + new_resolvs[i].(string) + "\n"
|
if err := writeFile(configPath, newConfigContent); err != nil {
|
||||||
}
|
return nil, fmt.Errorf("failed to write updated config to %s: %w", configPath, err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
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)
|
// 3. Update the resolv file
|
||||||
if err != nil {
|
if newResolvs, ok := new_settings["resolv"].([]any); ok {
|
||||||
return old_settings, err
|
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 {
|
// 4. Update the hosts file
|
||||||
return old_settings, err
|
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
|
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 {
|
func DecodeHosts(input string) map[string]any {
|
||||||
result := make(map[string]any)
|
result := make(map[string]any)
|
||||||
pairs := strings.SplitSeq(input, "\n")
|
lines := strings.Split(input, "\n")
|
||||||
|
|
||||||
for pair := range pairs {
|
for _, line := range lines {
|
||||||
if pair == "" {
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
kv := strings.Split(pair, " ")
|
|
||||||
if len(kv) == 2 {
|
key := fields[0]
|
||||||
key := strings.TrimSpace(kv[0])
|
if len(fields) == 1 {
|
||||||
value := strings.TrimSpace(kv[1])
|
result[key] = []string{}
|
||||||
if key != "" {
|
} else if len(fields) == 2 {
|
||||||
result[key] = value
|
result[key] = fields[1]
|
||||||
}
|
} else {
|
||||||
continue
|
result[key] = fields[1:]
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
+8
-12
@@ -4,19 +4,15 @@ import "strings"
|
|||||||
|
|
||||||
func DecodeResolvs(input string) []string {
|
func DecodeResolvs(input string) []string {
|
||||||
var result []string
|
var result []string
|
||||||
pairs := strings.SplitSeq(input, "\n")
|
lines := strings.Split(input, "\n")
|
||||||
|
|
||||||
for pair := range pairs {
|
for _, line := range lines {
|
||||||
if pair == "" {
|
trimmedLine := strings.TrimSpace(line)
|
||||||
continue
|
if strings.HasPrefix(trimmedLine, "nameserver ") {
|
||||||
}
|
value := strings.TrimPrefix(trimmedLine, "nameserver ")
|
||||||
kv := strings.SplitN(pair, " ", 2)
|
if value != "" {
|
||||||
if len(kv) != 2 {
|
result = append(result, value)
|
||||||
continue
|
}
|
||||||
}
|
|
||||||
value := strings.TrimSpace(kv[1])
|
|
||||||
if value != "" {
|
|
||||||
result = append(result, value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -4,20 +4,22 @@ import "strings"
|
|||||||
|
|
||||||
func DecodeSettings(input string, pairDelim string, kvDelim string) map[string]string {
|
func DecodeSettings(input string, pairDelim string, kvDelim string) map[string]string {
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
pairs := strings.SplitSeq(input, pairDelim)
|
pairs := strings.Split(input, pairDelim)
|
||||||
|
|
||||||
for pair := range pairs {
|
for _, pair := range pairs {
|
||||||
if pair == "" {
|
trimmedPair := strings.TrimSpace(pair)
|
||||||
|
if trimmedPair == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
kv := strings.SplitN(pair, kvDelim, 2)
|
|
||||||
|
kv := strings.SplitN(trimmedPair, kvDelim, 2)
|
||||||
if len(kv) != 2 {
|
if len(kv) != 2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
key := strings.TrimSpace(kv[0])
|
key := strings.TrimSpace(kv[0])
|
||||||
value := strings.TrimSpace(kv[1])
|
|
||||||
if key != "" {
|
if key != "" {
|
||||||
result[key] = value
|
result[key] = strings.TrimSpace(kv[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
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
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RestartDnsmasq() {
|
func RestartDnsmasq() error {
|
||||||
|
var cmd *exec.Cmd
|
||||||
switch os.Getenv("SYSTEM_INITIALIZATION") {
|
switch os.Getenv("SYSTEM_INITIALIZATION") {
|
||||||
case "rc":
|
case "rc":
|
||||||
exec.Command("rc", "restart", "dnsmasq").Run()
|
cmd = exec.Command("rc", "restart", "dnsmasq")
|
||||||
case "openrc":
|
case "openrc":
|
||||||
exec.Command("rc-service", "dnsmasq", "restart").Run()
|
cmd = exec.Command("rc-service", "dnsmasq", "restart")
|
||||||
case "systemd":
|
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