Compare commits
3 Commits
2470f121e2
...
c5279243c0
| Author | SHA1 | Date | |
|---|---|---|---|
| c5279243c0 | |||
| f96aaf1e96 | |||
| f3f1c0a0c8 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -44,3 +44,7 @@ vendor/
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Kubernetes secrets (never commit these!)
|
||||
k8s/overlays/*/secrets.yaml
|
||||
!k8s/overlays/*/secrets.example.yaml
|
||||
|
||||
42
README.md
42
README.md
@@ -77,9 +77,11 @@ TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12
|
||||
|
||||
### Deployment
|
||||
|
||||
#### Docker Compose (Recommended for single node)
|
||||
|
||||
```bash
|
||||
# Clone and start
|
||||
git clone <repo>
|
||||
git clone https://git.dws.rip/DWS/dyn.git
|
||||
cd dyn
|
||||
docker-compose up -d
|
||||
|
||||
@@ -90,6 +92,44 @@ docker-compose logs -f
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
#### Kubernetes / K3s
|
||||
|
||||
For production deployments on Kubernetes or K3s clusters:
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://git.dws.rip/DWS/dyn.git
|
||||
cd dyn
|
||||
|
||||
# Create your secrets file
|
||||
cp k8s/overlays/production/secrets.example.yaml k8s/overlays/production/secrets.yaml
|
||||
# Edit secrets.yaml with your actual Technitium credentials
|
||||
|
||||
# Deploy with kustomize
|
||||
kubectl apply -k k8s/overlays/production
|
||||
|
||||
# Or with kubectl 1.14+ (built-in kustomize)
|
||||
kubectl apply -k k8s/overlays/production
|
||||
|
||||
# Check deployment status
|
||||
kubectl get pods -n dyn-ddns
|
||||
kubectl logs -n dyn-ddns -l app=dyn-ddns
|
||||
|
||||
# Delete deployment
|
||||
kubectl delete -k k8s/overlays/production
|
||||
```
|
||||
|
||||
**Kustomize Overlays:**
|
||||
- `k8s/overlays/production` - Production setup (2 replicas, higher resource limits)
|
||||
- `k8s/overlays/staging` - Staging environment (1 replica, relaxed rate limits)
|
||||
|
||||
**Requirements:**
|
||||
- Cert-manager (for TLS certificates via Let's Encrypt)
|
||||
- Traefik or NGINX ingress controller
|
||||
- Persistent storage class (for SQLite database)
|
||||
|
||||
See [k8s/README.md](k8s/README.md) for detailed Kubernetes deployment documentation.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Web UI
|
||||
|
||||
17
go.mod
17
go.mod
@@ -1,6 +1,6 @@
|
||||
module git.dws.rip/DWS/dyn
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
@@ -8,6 +8,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/TwiN/go-away v1.8.1 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
@@ -31,12 +32,12 @@ require (
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
)
|
||||
|
||||
16
go.sum
16
go.sum
@@ -1,3 +1,5 @@
|
||||
github.com/TwiN/go-away v1.8.1 h1:zbbr0ISBkDSbnUFHrnRUhbCR/7+9ONMWtIi1BiQWX8Y=
|
||||
github.com/TwiN/go-away v1.8.1/go.mod h1:nSQEvd/FYBNmnC27RGJdPi91LXYMG8SrRc1o1w+VmKY=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
@@ -69,19 +71,33 @@ golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
147
internal/handlers/custom_filter.go
Normal file
147
internal/handlers/custom_filter.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// CustomFilter contains terms related to DWS and Tanishq Dubey that should be blocked
|
||||
type CustomFilter struct {
|
||||
blockedTerms []string
|
||||
}
|
||||
|
||||
// NewCustomFilter creates a new custom filter with DWS and personal name variations
|
||||
func NewCustomFilter() *CustomFilter {
|
||||
return &CustomFilter{
|
||||
blockedTerms: []string{
|
||||
// DWS variations
|
||||
"dws",
|
||||
"dubey",
|
||||
"dubeyweb",
|
||||
"dubeywebservices",
|
||||
"dubeyweb services",
|
||||
"dubey-engineering",
|
||||
"dubeyengineering",
|
||||
"dwsengineering",
|
||||
"dws-engineering",
|
||||
"dws-engineering-llc",
|
||||
"dwsengineeringllc",
|
||||
"dwsllc",
|
||||
"dws-llc",
|
||||
"webservices",
|
||||
"web-services",
|
||||
"dubeycorp",
|
||||
"dubey-corp",
|
||||
"dubeyinc",
|
||||
"dubey-inc",
|
||||
|
||||
// Tanishq Dubey variations
|
||||
"tanishq",
|
||||
"tanishqdubey",
|
||||
"tanishq-dubey",
|
||||
"tdubey",
|
||||
"t-dubey",
|
||||
"tanishq-d",
|
||||
"tdub",
|
||||
"tanish",
|
||||
"dubey-t",
|
||||
"dubeytanishq",
|
||||
"dubey-tanishq",
|
||||
|
||||
// Leet speak variations
|
||||
"dub3y",
|
||||
"dub3yweb",
|
||||
"t4nishq",
|
||||
"t4n1shq",
|
||||
"tan1shq",
|
||||
"dub3y3ng1n33r1ng",
|
||||
"dw5",
|
||||
"dw$",
|
||||
"dub3yc0rp",
|
||||
|
||||
// Common combinations
|
||||
"dubeydns",
|
||||
"dubey-ddns",
|
||||
"dwsdns",
|
||||
"dws-ddns",
|
||||
"tanishqdns",
|
||||
"tanishq-ddns",
|
||||
"tdubeydns",
|
||||
"tdubey-ddns",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsBlocked checks if the given text contains any blocked terms
|
||||
func (cf *CustomFilter) IsBlocked(text string) bool {
|
||||
normalized := cf.normalize(text)
|
||||
|
||||
for _, term := range cf.blockedTerms {
|
||||
if strings.Contains(normalized, term) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// normalize prepares text for comparison by:
|
||||
// - Converting to lowercase
|
||||
// - Removing common separators
|
||||
// - Converting leet speak to normal text
|
||||
func (cf *CustomFilter) normalize(text string) string {
|
||||
// Convert to lowercase
|
||||
text = strings.ToLower(text)
|
||||
|
||||
// Remove separators
|
||||
replacer := strings.NewReplacer(
|
||||
"-", "",
|
||||
"_", "",
|
||||
".", "",
|
||||
" ", "",
|
||||
)
|
||||
text = replacer.Replace(text)
|
||||
|
||||
// Convert leet speak
|
||||
text = cf.leetToNormal(text)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// leetToNormal converts common leet speak characters to normal letters
|
||||
func (cf *CustomFilter) leetToNormal(text string) string {
|
||||
replacements := map[rune]rune{
|
||||
'0': 'o',
|
||||
'1': 'i',
|
||||
'3': 'e',
|
||||
'4': 'a',
|
||||
'5': 's',
|
||||
'6': 'g',
|
||||
'7': 't',
|
||||
'8': 'b',
|
||||
'9': 'g',
|
||||
'@': 'a',
|
||||
'$': 's',
|
||||
'!': 'i',
|
||||
'|': 'i',
|
||||
'+': 't',
|
||||
}
|
||||
|
||||
result := make([]rune, len(text))
|
||||
for i, char := range text {
|
||||
if replacement, ok := replacements[char]; ok {
|
||||
result[i] = replacement
|
||||
} else {
|
||||
result[i] = unicode.ToLower(char)
|
||||
}
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// GetBlockedTerms returns a copy of the blocked terms list (for testing/debugging)
|
||||
func (cf *CustomFilter) GetBlockedTerms() []string {
|
||||
terms := make([]string, len(cf.blockedTerms))
|
||||
copy(terms, cf.blockedTerms)
|
||||
return terms
|
||||
}
|
||||
@@ -12,9 +12,15 @@ import (
|
||||
"git.dws.rip/DWS/dyn/internal/database"
|
||||
"git.dws.rip/DWS/dyn/internal/dns"
|
||||
"git.dws.rip/DWS/dyn/internal/models"
|
||||
"github.com/TwiN/go-away"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var (
|
||||
profanityDetector = goaway.NewProfanityDetector()
|
||||
customFilter = NewCustomFilter()
|
||||
)
|
||||
|
||||
type WebHandler struct {
|
||||
db *database.DB
|
||||
config *config.Config
|
||||
@@ -49,6 +55,16 @@ func (h *WebHandler) ClaimSpace(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if profanityDetector.IsProfane(subdomain) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Subdomain contains inappropriate content"})
|
||||
return
|
||||
}
|
||||
|
||||
if customFilter.IsBlocked(subdomain) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Subdomain is reserved"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -86,6 +102,24 @@ func (h *WebHandler) CheckSubdomain(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if profanityDetector.IsProfane(subdomain) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"available": false,
|
||||
"subdomain": subdomain,
|
||||
"reason": "inappropriate",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if customFilter.IsBlocked(subdomain) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"available": false,
|
||||
"subdomain": subdomain,
|
||||
"reason": "reserved",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
365
k8s/README.md
Normal file
365
k8s/README.md
Normal file
@@ -0,0 +1,365 @@
|
||||
# Kubernetes Deployment
|
||||
|
||||
This directory contains Kubernetes manifests for deploying the DWS Dynamic DNS service on K3s, Kubernetes, or any K8s-compatible platform.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
k8s/
|
||||
├── base/ # Base manifests (don't edit directly)
|
||||
│ ├── deployment.yaml # Deployment, Service, PVC
|
||||
│ ├── configmap.yaml # Non-sensitive configuration
|
||||
│ ├── secrets.yaml # Sensitive configuration (placeholders)
|
||||
│ ├── ingress.yaml # Ingress with TLS
|
||||
│ └── kustomization.yaml # Base kustomization
|
||||
│
|
||||
├── overlays/
|
||||
│ ├── production/ # Production environment
|
||||
│ │ ├── kustomization.yaml # Production-specific settings
|
||||
│ │ ├── deployment-patch.yaml # Resource adjustments
|
||||
│ │ ├── namespace.yaml # Production namespace
|
||||
│ │ ├── secrets.yaml # Production secrets (gitignored)
|
||||
│ │ └── secrets.example.yaml # Example secrets template
|
||||
│ │
|
||||
│ └── staging/ # Staging environment
|
||||
│ ├── kustomization.yaml # Staging-specific settings
|
||||
│ ├── deployment-patch.yaml # Single replica, lower resources
|
||||
│ ├── namespace.yaml # Staging namespace
|
||||
│ ├── secrets.yaml # Staging secrets (gitignored)
|
||||
│ └── secrets.example.yaml # Example secrets template
|
||||
│
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Kubernetes 1.21+ or K3s cluster
|
||||
- kubectl configured with cluster access
|
||||
- cert-manager installed (for TLS certificates)
|
||||
- Ingress controller (Traefik, NGINX, etc.)
|
||||
- Storage class for persistent volumes
|
||||
|
||||
### Production Deployment
|
||||
|
||||
```bash
|
||||
# 1. Clone and enter directory
|
||||
git clone https://git.dws.rip/DWS/dyn.git
|
||||
cd dyn/k8s
|
||||
|
||||
# 2. Create production secrets
|
||||
cp overlays/production/secrets.example.yaml overlays/production/secrets.yaml
|
||||
|
||||
# 3. Edit secrets with your Technitium credentials
|
||||
# Replace 'your-production-api-token-here' with actual token
|
||||
nano overlays/production/secrets.yaml
|
||||
|
||||
# 4. Deploy to production
|
||||
kubectl apply -k overlays/production
|
||||
|
||||
# 5. Verify deployment
|
||||
kubectl get pods -n dyn-ddns
|
||||
kubectl get svc -n dyn-ddns
|
||||
kubectl get ingress -n dyn-ddns
|
||||
|
||||
# 6. Check logs
|
||||
kubectl logs -n dyn-ddns -l app.kubernetes.io/name=dyn-ddns -f
|
||||
```
|
||||
|
||||
### Staging Deployment
|
||||
|
||||
```bash
|
||||
# Similar process for staging
|
||||
cp overlays/staging/secrets.example.yaml overlays/staging/secrets.yaml
|
||||
# Edit with staging credentials
|
||||
kubectl apply -k overlays/staging
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Secrets (Required)
|
||||
|
||||
Create `overlays/production/secrets.yaml`:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dyn-ddns-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Choose ONE authentication method:
|
||||
|
||||
# Method 1: API Token (recommended)
|
||||
TECHNITIUM_TOKEN: "your-actual-api-token-here"
|
||||
|
||||
# Method 2: Username/Password
|
||||
# TECHNITIUM_USERNAME: "admin"
|
||||
# TECHNITIUM_PASSWORD: "your-password"
|
||||
|
||||
# Optional: Trusted proxies
|
||||
# TRUSTED_PROXIES: "10.0.0.0/8,172.16.0.0/12"
|
||||
```
|
||||
|
||||
**Important:** Never commit secrets.yaml to git. It's already in .gitignore.
|
||||
|
||||
### ConfigMap (Optional Overrides)
|
||||
|
||||
Edit `overlays/production/kustomization.yaml`:
|
||||
|
||||
```yaml
|
||||
configMapGenerator:
|
||||
- name: dyn-ddns-config
|
||||
behavior: merge
|
||||
literals:
|
||||
- TECHNITIUM_URL=https://dns.dws.rip
|
||||
- BASE_DOMAIN=dws.rip
|
||||
- SPACE_SUBDOMAIN=space
|
||||
- RATE_LIMIT_PER_IP=10
|
||||
- RATE_LIMIT_PER_TOKEN=1
|
||||
```
|
||||
|
||||
### Ingress Customization
|
||||
|
||||
By default, the ingress is configured for **Traefik** with cert-manager:
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
```
|
||||
|
||||
For **NGINX** ingress, change annotations in `base/ingress.yaml`:
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components
|
||||
|
||||
1. **Deployment** - Runs the DDNS bridge container
|
||||
- Replicas: 2 (production), 1 (staging)
|
||||
- Resource limits configurable per overlay
|
||||
- Health checks (liveness & readiness probes)
|
||||
|
||||
2. **Service** - ClusterIP exposing port 80
|
||||
|
||||
3. **Ingress** - TLS termination at edge
|
||||
- Host: dyn.dws.rip (customize as needed)
|
||||
- Automatic certificate via cert-manager
|
||||
|
||||
4. **PersistentVolumeClaim** - SQLite database storage
|
||||
- Size: 1Gi (adjustable)
|
||||
- AccessMode: ReadWriteOnce
|
||||
|
||||
### Resource Requirements
|
||||
|
||||
**Production:**
|
||||
- CPU: 200m request / 1000m limit
|
||||
- Memory: 128Mi request / 512Mi limit
|
||||
- Replicas: 2
|
||||
|
||||
**Staging:**
|
||||
- CPU: 100m request / 500m limit
|
||||
- Memory: 64Mi request / 256Mi limit
|
||||
- Replicas: 1
|
||||
|
||||
## Operations
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# All pods
|
||||
kubectl logs -n dyn-ddns -l app.kubernetes.io/name=dyn-ddns
|
||||
|
||||
# Specific pod
|
||||
kubectl logs -n dyn-ddns -f deployment/prod-dyn-ddns
|
||||
|
||||
# Previous container logs (after restart)
|
||||
kubectl logs -n dyn-ddns --previous deployment/prod-dyn-ddns
|
||||
```
|
||||
|
||||
### Scale Deployment
|
||||
|
||||
```bash
|
||||
# Scale to 3 replicas
|
||||
kubectl scale deployment -n dyn-ddns prod-dyn-ddns --replicas=3
|
||||
|
||||
# Edit deployment directly
|
||||
kubectl edit deployment -n dyn-ddns prod-dyn-ddns
|
||||
```
|
||||
|
||||
### Database Backup
|
||||
|
||||
The SQLite database is stored in the persistent volume at `/data/dyn.db`:
|
||||
|
||||
```bash
|
||||
# Find the pod
|
||||
POD=$(kubectl get pod -n dyn-ddns -l app.kubernetes.io/name=dyn-ddns -o jsonpath='{.items[0].metadata.name}')
|
||||
|
||||
# Copy database locally
|
||||
kubectl cp dyn-ddns/$POD:/data/dyn.db ./dyn-backup-$(date +%Y%m%d).db
|
||||
|
||||
# Or exec into pod
|
||||
kubectl exec -it -n dyn-ddns $POD -- sh
|
||||
# Then: sqlite3 /data/dyn.db "SELECT * FROM spaces;"
|
||||
```
|
||||
|
||||
### Update Deployment
|
||||
|
||||
```bash
|
||||
# Update to latest image
|
||||
kubectl rollout restart deployment -n dyn-ddns prod-dyn-ddns
|
||||
|
||||
# Or set specific image tag
|
||||
kubectl set image deployment -n dyn-ddns prod-dyn-ddns dyn-ddns=git.dws.rip/DWS/dyn:v1.0.0
|
||||
|
||||
# Monitor rollout
|
||||
kubectl rollout status deployment -n dyn-ddns prod-dyn-ddns
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Pod stuck in Pending:**
|
||||
```bash
|
||||
kubectl describe pod -n dyn-ddns <pod-name>
|
||||
# Check: Storage class available? PV provisioned?
|
||||
```
|
||||
|
||||
**Pod crash looping:**
|
||||
```bash
|
||||
kubectl logs -n dyn-ddns --previous <pod-name>
|
||||
# Check: Secrets configured? Technitium URL reachable?
|
||||
```
|
||||
|
||||
**Ingress not working:**
|
||||
```bash
|
||||
kubectl describe ingress -n dyn-ddns prod-dyn-ddns
|
||||
kubectl get certificate -n dyn-ddns
|
||||
# Check: DNS pointing to ingress controller? Cert-manager working?
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Multi-Environment Setup
|
||||
|
||||
Deploy to multiple environments:
|
||||
|
||||
```bash
|
||||
# Staging
|
||||
kubectl apply -k overlays/staging
|
||||
|
||||
# Production
|
||||
kubectl apply -k overlays/production
|
||||
|
||||
# Verify both
|
||||
kubectl get pods --all-namespaces -l app.kubernetes.io/name=dyn-ddns
|
||||
```
|
||||
|
||||
### Custom Overlay
|
||||
|
||||
Create your own overlay for specific needs:
|
||||
|
||||
```bash
|
||||
mkdir overlays/custom
|
||||
cat > overlays/custom/kustomization.yaml << 'EOF'
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
namespace: my-namespace
|
||||
|
||||
configMapGenerator:
|
||||
- name: dyn-ddns-config
|
||||
behavior: merge
|
||||
literals:
|
||||
- TECHNITIUM_URL=https://my-dns-server.example.com
|
||||
- RATE_LIMIT_PER_IP=5
|
||||
EOF
|
||||
|
||||
kubectl apply -k overlays/custom
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
The deployment exposes standard metrics. Add Prometheus scraping:
|
||||
|
||||
```yaml
|
||||
# Add to deployment-patch.yaml
|
||||
metadata:
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Secrets Management**
|
||||
- Never commit secrets to git
|
||||
- Consider using external secrets operator (Vault, Sealed Secrets)
|
||||
- Rotate Technitium API tokens regularly
|
||||
|
||||
2. **Network Policies**
|
||||
```yaml
|
||||
# Example network policy
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: dyn-ddns-netpol
|
||||
namespace: dyn-ddns
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: dyn-ddns
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: ingress-nginx
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
egress:
|
||||
- to: [] # Allow all egress (for Technitium API)
|
||||
```
|
||||
|
||||
3. **Pod Security**
|
||||
- Container runs as non-root (in Dockerfile)
|
||||
- Read-only root filesystem recommended
|
||||
- Drop all capabilities
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Regular Tasks
|
||||
|
||||
1. **Backup database weekly**
|
||||
2. **Monitor rate limit metrics**
|
||||
3. **Review access logs**
|
||||
4. **Update base image for security patches**
|
||||
|
||||
### Version Upgrades
|
||||
|
||||
1. Update image tag in overlay kustomization
|
||||
2. Apply changes: `kubectl apply -k overlays/production`
|
||||
3. Verify rollout: `kubectl rollout status ...`
|
||||
4. Monitor for errors in logs
|
||||
|
||||
## Support
|
||||
|
||||
For issues specific to Kubernetes deployment:
|
||||
1. Check pod logs: `kubectl logs ...`
|
||||
2. Describe resources: `kubectl describe ...`
|
||||
3. Check ingress controller logs
|
||||
4. Verify cert-manager is issuing certificates
|
||||
13
k8s/base/configmap.yaml
Normal file
13
k8s/base/configmap.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: dyn-ddns-config
|
||||
data:
|
||||
SERVER_PORT: "8080"
|
||||
DATABASE_PATH: "/data/dyn.db"
|
||||
BASE_DOMAIN: "dws.rip"
|
||||
SPACE_SUBDOMAIN: "space"
|
||||
RATE_LIMIT_PER_IP: "10"
|
||||
RATE_LIMIT_PER_TOKEN: "1"
|
||||
# TECHNITIUM_URL: "https://dns.dws.rip"
|
||||
# Add your Technitium URL above or in overlays
|
||||
86
k8s/base/deployment.yaml
Normal file
86
k8s/base/deployment.yaml
Normal file
@@ -0,0 +1,86 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dyn-ddns
|
||||
labels:
|
||||
app: dyn-ddns
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dyn-ddns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dyn-ddns
|
||||
spec:
|
||||
containers:
|
||||
- name: dyn-ddns
|
||||
image: git.dws.rip/DWS/dyn:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: dyn-ddns-config
|
||||
- secretRef:
|
||||
name: dyn-ddns-secrets
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: dyn-ddns-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dyn-ddns
|
||||
labels:
|
||||
app: dyn-ddns
|
||||
spec:
|
||||
selector:
|
||||
app: dyn-ddns
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
name: http
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: dyn-ddns-data
|
||||
labels:
|
||||
app: dyn-ddns
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
30
k8s/base/ingress.yaml
Normal file
30
k8s/base/ingress.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: dyn-ddns
|
||||
annotations:
|
||||
# Traefik
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
|
||||
# NGINX (uncomment if using NGINX ingress)
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
# nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- dyn.dws.rip
|
||||
secretName: dyn-ddns-tls
|
||||
rules:
|
||||
- host: dyn.dws.rip
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: dyn-ddns
|
||||
port:
|
||||
number: 80
|
||||
17
k8s/base/kustomization.yaml
Normal file
17
k8s/base/kustomization.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- configmap.yaml
|
||||
- ingress.yaml
|
||||
|
||||
commonLabels:
|
||||
app.kubernetes.io/name: dyn-ddns
|
||||
app.kubernetes.io/part-of: dws-dns
|
||||
|
||||
namespace: default
|
||||
|
||||
images:
|
||||
- name: git.dws.rip/DWS/dyn
|
||||
newTag: latest
|
||||
17
k8s/base/secrets.yaml
Normal file
17
k8s/base/secrets.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dyn-ddns-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Choose ONE authentication method:
|
||||
|
||||
# Method 1: API Token (recommended)
|
||||
# TECHNITIUM_TOKEN: "your-api-token-here"
|
||||
|
||||
# Method 2: Username/Password
|
||||
# TECHNITIUM_USERNAME: "admin"
|
||||
# TECHNITIUM_PASSWORD: "your-password"
|
||||
|
||||
# Optional: Trusted proxies (comma-separated)
|
||||
# TRUSTED_PROXIES: "10.0.0.0/8,172.16.0.0/12"
|
||||
16
k8s/overlays/production/deployment-patch.yaml
Normal file
16
k8s/overlays/production/deployment-patch.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dyn-ddns
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: dyn-ddns
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "200m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "1000m"
|
||||
28
k8s/overlays/production/kustomization.yaml
Normal file
28
k8s/overlays/production/kustomization.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- ../../base
|
||||
- secrets.yaml
|
||||
|
||||
namePrefix: prod-
|
||||
namespace: dyn-ddns
|
||||
|
||||
commonLabels:
|
||||
environment: production
|
||||
|
||||
configMapGenerator:
|
||||
- name: dyn-ddns-config
|
||||
behavior: merge
|
||||
literals:
|
||||
- TECHNITIUM_URL=https://dns.dws.rip
|
||||
- RATE_LIMIT_PER_IP=10
|
||||
- RATE_LIMIT_PER_TOKEN=1
|
||||
|
||||
patchesStrategicMerge:
|
||||
- deployment-patch.yaml
|
||||
|
||||
replicas:
|
||||
- name: dyn-ddns
|
||||
count: 2
|
||||
7
k8s/overlays/production/namespace.yaml
Normal file
7
k8s/overlays/production/namespace.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: dyn-ddns
|
||||
labels:
|
||||
app.kubernetes.io/name: dyn-ddns
|
||||
app.kubernetes.io/part-of: dws-dns
|
||||
12
k8s/overlays/production/secrets.example.yaml
Normal file
12
k8s/overlays/production/secrets.example.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dyn-ddns-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Replace with your actual Technitium API token
|
||||
TECHNITIUM_TOKEN: "your-production-api-token-here"
|
||||
|
||||
# Or use username/password (not recommended for production)
|
||||
# TECHNITIUM_USERNAME: "admin"
|
||||
# TECHNITIUM_PASSWORD: "your-password"
|
||||
17
k8s/overlays/staging/deployment-patch.yaml
Normal file
17
k8s/overlays/staging/deployment-patch.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dyn-ddns
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: dyn-ddns
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "500m"
|
||||
24
k8s/overlays/staging/kustomization.yaml
Normal file
24
k8s/overlays/staging/kustomization.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- ../../base
|
||||
- secrets.yaml
|
||||
|
||||
namePrefix: staging-
|
||||
namespace: dyn-ddns-staging
|
||||
|
||||
commonLabels:
|
||||
environment: staging
|
||||
|
||||
configMapGenerator:
|
||||
- name: dyn-ddns-config
|
||||
behavior: merge
|
||||
literals:
|
||||
- TECHNITIUM_URL=https://dns-staging.dws.rip
|
||||
- RATE_LIMIT_PER_IP=100
|
||||
- RATE_LIMIT_PER_TOKEN=10
|
||||
|
||||
patchesStrategicMerge:
|
||||
- deployment-patch.yaml
|
||||
7
k8s/overlays/staging/namespace.yaml
Normal file
7
k8s/overlays/staging/namespace.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: dyn-ddns-staging
|
||||
labels:
|
||||
app.kubernetes.io/name: dyn-ddns
|
||||
app.kubernetes.io/part-of: dws-dns
|
||||
8
k8s/overlays/staging/secrets.example.yaml
Normal file
8
k8s/overlays/staging/secrets.example.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dyn-ddns-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Replace with your staging Technitium credentials
|
||||
TECHNITIUM_TOKEN: "your-staging-api-token-here"
|
||||
@@ -33,6 +33,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
availabilityStatus.textContent = '✓ Available';
|
||||
availabilityStatus.className = 'status available';
|
||||
claimBtn.disabled = false;
|
||||
} else if (data.reason === 'inappropriate') {
|
||||
availabilityStatus.textContent = '✗ Contains inappropriate content';
|
||||
availabilityStatus.className = 'status taken';
|
||||
claimBtn.disabled = true;
|
||||
} else if (data.reason === 'reserved') {
|
||||
availabilityStatus.textContent = '✗ This subdomain is reserved';
|
||||
availabilityStatus.className = 'status taken';
|
||||
claimBtn.disabled = true;
|
||||
} else {
|
||||
availabilityStatus.textContent = '✗ Already taken';
|
||||
availabilityStatus.className = 'status taken';
|
||||
|
||||
Reference in New Issue
Block a user