feat: Squid Forward-Proxy — vollständig (Renderer + Handler + UI)
Stub raus, vollständig implementiert: * internal/services/forwardproxy: CRUD-Repo gegen forward_proxy_acls (priority desc, action allow|deny). * internal/handlers/forwardproxy.go: REST /api/v1/forward-proxy/acls mit Validation (acl_type-Whitelist verhindert Squid-Reload-Crash bei Tippfehlern). Auto-Reload nach jeder Mutation. * internal/squid/squid.cfg.tpl + squid.go: Renderer schreibt /etc/edgeguard/squid/squid.conf, atomic + Symlink von /etc/squid/squid.conf (Squid liest Distro-Pfad — gleicher Pattern-Fix wie wg-quick). cache_dir 100MB, cache_mem 64MB, http_port 3128. Default-Policy: nur localnet (10/8, 172.16/12, 192.168/16) — verhindert Open-Relay, falls Operator keine ACLs anlegt. * main.go: forwardproxy-Repo + squid-Reloader instanziiert + Handler registriert. * render.go: squid.New() bekommt Pool (war () vorher, Stub-Signatur). * postinst sudoers: edgeguard darf systemctl reload squid.service. * Frontend /forward-proxy: PageHeader + DataTable + ACL-Modal mit acl_type-Dropdown (13 Squid-Vokabular-Typen), action-Select, Priority. Sidebar-Eintrag unter Security. * i18n DE/EN für fwd.* Block + nav.forwardProxy. Verified end-to-end: ACL-Insert via SQL, render → squid reload → curl -x http://127.0.0.1:3128 http://example.com/ → 200. Version 1.0.26. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
|||||||
firewallrender "git.netcell-it.de/projekte/edgeguard-native/internal/firewall"
|
firewallrender "git.netcell-it.de/projekte/edgeguard-native/internal/firewall"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/haproxy"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/haproxy"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers"
|
||||||
|
squidrender "git.netcell-it.de/projekte/edgeguard-native/internal/squid"
|
||||||
wgrender "git.netcell-it.de/projekte/edgeguard-native/internal/wireguard"
|
wgrender "git.netcell-it.de/projekte/edgeguard-native/internal/wireguard"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/acme"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/acme"
|
||||||
@@ -29,6 +30,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/backends"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/backends"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/domains"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/domains"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/firewall"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/firewall"
|
||||||
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/forwardproxy"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/ipaddresses"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/ipaddresses"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/networkifs"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/networkifs"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/routingrules"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/routingrules"
|
||||||
@@ -39,7 +41,7 @@ import (
|
|||||||
wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard"
|
wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.25"
|
var version = "1.0.26"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
||||||
@@ -141,6 +143,7 @@ func main() {
|
|||||||
secretsBox := secrets.New("")
|
secretsBox := secrets.New("")
|
||||||
wgIfaces := wgsvc.NewInterfacesRepo(pool)
|
wgIfaces := wgsvc.NewInterfacesRepo(pool)
|
||||||
wgPeers := wgsvc.NewPeersRepo(pool)
|
wgPeers := wgsvc.NewPeersRepo(pool)
|
||||||
|
fwdProxyRepo := forwardproxy.New(pool)
|
||||||
|
|
||||||
// ACME (Let's Encrypt). Email comes from setup.json — the
|
// ACME (Let's Encrypt). Email comes from setup.json — the
|
||||||
// wizard collects acme_email and the issuer registers an
|
// wizard collects acme_email and the issuer registers an
|
||||||
@@ -182,6 +185,13 @@ func main() {
|
|||||||
return wgrender.New(pool, secretsBox).Render(ctx)
|
return wgrender.New(pool, secretsBox).Render(ctx)
|
||||||
}
|
}
|
||||||
handlers.NewWireguardHandler(wgIfaces, wgPeers, secretsBox, auditRepo, nodeID, wgReloader).Register(authed)
|
handlers.NewWireguardHandler(wgIfaces, wgPeers, secretsBox, auditRepo, nodeID, wgReloader).Register(authed)
|
||||||
|
|
||||||
|
// Squid forward-proxy reload — re-render squid.conf + reload
|
||||||
|
// squid.service. sudoers im postinst whitelistet das.
|
||||||
|
squidReloader := func(ctx context.Context) error {
|
||||||
|
return squidrender.New(pool).Render(ctx)
|
||||||
|
}
|
||||||
|
handlers.NewForwardProxyHandler(fwdProxyRepo, auditRepo, nodeID, squidReloader).Register(authed)
|
||||||
}
|
}
|
||||||
|
|
||||||
mountUI(r)
|
mountUI(r)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.25"
|
var version = "1.0.26"
|
||||||
|
|
||||||
const usage = `edgeguard-ctl — EdgeGuard CLI
|
const usage = `edgeguard-ctl — EdgeGuard CLI
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func cmdRenderConfig(args []string) int {
|
|||||||
|
|
||||||
hap := haproxy.New(pool)
|
hap := haproxy.New(pool)
|
||||||
fw := firewall.New(pool)
|
fw := firewall.New(pool)
|
||||||
sq := squid.New()
|
sq := squid.New(pool)
|
||||||
wg := wireguard.New(pool, secrets.New(""))
|
wg := wireguard.New(pool, secrets.New(""))
|
||||||
ub := unbound.New()
|
ub := unbound.New()
|
||||||
if skipReload {
|
if skipReload {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.25"
|
var version = "1.0.26"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// renewTickInterval — how often we re-evaluate expiring certs.
|
// renewTickInterval — how often we re-evaluate expiring certs.
|
||||||
|
|||||||
162
internal/handlers/forwardproxy.go
Normal file
162
internal/handlers/forwardproxy.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
||||||
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||||
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
|
||||||
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/forwardproxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ForwardProxyHandler struct {
|
||||||
|
Repo *forwardproxy.Repo
|
||||||
|
Audit *audit.Repo
|
||||||
|
NodeID string
|
||||||
|
Reloader func(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewForwardProxyHandler(repo *forwardproxy.Repo, a *audit.Repo, nodeID string, reloader func(context.Context) error) *ForwardProxyHandler {
|
||||||
|
return &ForwardProxyHandler{Repo: repo, Audit: a, NodeID: nodeID, Reloader: reloader}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ForwardProxyHandler) reload(ctx context.Context, op string) {
|
||||||
|
if h.Reloader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.Reloader(ctx); err != nil {
|
||||||
|
slog.Warn("squid: reload after mutation failed", "op", op, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ForwardProxyHandler) Register(rg *gin.RouterGroup) {
|
||||||
|
g := rg.Group("/forward-proxy/acls")
|
||||||
|
g.GET("", h.List)
|
||||||
|
g.POST("", h.Create)
|
||||||
|
g.GET("/:id", h.Get)
|
||||||
|
g.PUT("/:id", h.Update)
|
||||||
|
g.DELETE("/:id", h.Delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ForwardProxyHandler) List(c *gin.Context) {
|
||||||
|
out, err := h.Repo.List(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
response.Internal(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, gin.H{"acls": out})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ForwardProxyHandler) Get(c *gin.Context) {
|
||||||
|
id, ok := parseID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := h.Repo.Get(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, forwardproxy.ErrNotFound) {
|
||||||
|
response.NotFound(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Internal(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ForwardProxyHandler) Create(c *gin.Context) {
|
||||||
|
var req models.ForwardProxyACL
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validateACL(&req); err != nil {
|
||||||
|
response.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, err := h.Repo.Create(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
response.Internal(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "forward_proxy.acl.create", out.Name, out, h.NodeID)
|
||||||
|
response.Created(c, out)
|
||||||
|
h.reload(c.Request.Context(), "create")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ForwardProxyHandler) Update(c *gin.Context) {
|
||||||
|
id, ok := parseID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req models.ForwardProxyACL
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validateACL(&req); err != nil {
|
||||||
|
response.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, err := h.Repo.Update(c.Request.Context(), id, req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, forwardproxy.ErrNotFound) {
|
||||||
|
response.NotFound(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Internal(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "forward_proxy.acl.update", out.Name, out, h.NodeID)
|
||||||
|
response.OK(c, out)
|
||||||
|
h.reload(c.Request.Context(), "update")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ForwardProxyHandler) Delete(c *gin.Context) {
|
||||||
|
id, ok := parseID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.Repo.Delete(c.Request.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, forwardproxy.ErrNotFound) {
|
||||||
|
response.NotFound(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Internal(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "forward_proxy.acl.delete", strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
|
||||||
|
response.NoContent(c)
|
||||||
|
h.reload(c.Request.Context(), "delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateACL prüft Name (squid-konform), action, acl_type. Squid
|
||||||
|
// nimmt viele Typen — wir whitelisten die, die in einem Forward-
|
||||||
|
// Proxy-Setup üblich sind, damit Tippfehler nicht beim reload
|
||||||
|
// crashen.
|
||||||
|
func validateACL(a *models.ForwardProxyACL) error {
|
||||||
|
if a.Name == "" {
|
||||||
|
return errors.New("name required")
|
||||||
|
}
|
||||||
|
switch a.Action {
|
||||||
|
case "allow", "deny":
|
||||||
|
default:
|
||||||
|
return errors.New("action must be allow or deny")
|
||||||
|
}
|
||||||
|
switch a.ACLType {
|
||||||
|
case "src", "dst", "dstdomain", "srcdomain", "port",
|
||||||
|
"proto", "method", "time", "url_regex", "urlpath_regex",
|
||||||
|
"dstdom_regex", "srcdom_regex", "browser":
|
||||||
|
default:
|
||||||
|
return errors.New("acl_type not supported (allowed: src, dst, dstdomain, srcdomain, port, proto, method, time, url_regex, urlpath_regex, dstdom_regex, srcdom_regex, browser)")
|
||||||
|
}
|
||||||
|
if a.Value == "" {
|
||||||
|
return errors.New("value required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
110
internal/services/forwardproxy/forwardproxy.go
Normal file
110
internal/services/forwardproxy/forwardproxy.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// Package forwardproxy provides CRUD against the forward_proxy_acls
|
||||||
|
// table. Renderer in internal/squid consumes the same rows to emit
|
||||||
|
// /etc/edgeguard/squid/squid.conf.
|
||||||
|
package forwardproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("forward-proxy ACL not found")
|
||||||
|
|
||||||
|
type Repo struct {
|
||||||
|
Pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
|
||||||
|
|
||||||
|
const baseSelect = `
|
||||||
|
SELECT id, name, acl_type, value, action, priority, active, comment,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM forward_proxy_acls
|
||||||
|
`
|
||||||
|
|
||||||
|
func (r *Repo) List(ctx context.Context) ([]models.ForwardProxyACL, error) {
|
||||||
|
rows, err := r.Pool.Query(ctx, baseSelect+" ORDER BY priority DESC, id ASC")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
out := make([]models.ForwardProxyACL, 0, 8)
|
||||||
|
for rows.Next() {
|
||||||
|
a, err := scan(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, *a)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) Get(ctx context.Context, id int64) (*models.ForwardProxyACL, error) {
|
||||||
|
row := r.Pool.QueryRow(ctx, baseSelect+" WHERE id = $1", id)
|
||||||
|
a, err := scan(row)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) Create(ctx context.Context, a models.ForwardProxyACL) (*models.ForwardProxyACL, error) {
|
||||||
|
row := r.Pool.QueryRow(ctx, `
|
||||||
|
INSERT INTO forward_proxy_acls (name, acl_type, value, action, priority, active, comment)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING id, name, acl_type, value, action, priority, active, comment,
|
||||||
|
created_at, updated_at`,
|
||||||
|
a.Name, a.ACLType, a.Value, a.Action, a.Priority, a.Active, a.Comment)
|
||||||
|
return scan(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) Update(ctx context.Context, id int64, a models.ForwardProxyACL) (*models.ForwardProxyACL, error) {
|
||||||
|
row := r.Pool.QueryRow(ctx, `
|
||||||
|
UPDATE forward_proxy_acls SET
|
||||||
|
name = $1, acl_type = $2, value = $3, action = $4,
|
||||||
|
priority = $5, active = $6, comment = $7,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $8
|
||||||
|
RETURNING id, name, acl_type, value, action, priority, active, comment,
|
||||||
|
created_at, updated_at`,
|
||||||
|
a.Name, a.ACLType, a.Value, a.Action, a.Priority, a.Active, a.Comment, id)
|
||||||
|
out, err := scan(row)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) Delete(ctx context.Context, id int64) error {
|
||||||
|
tag, err := r.Pool.Exec(ctx, `DELETE FROM forward_proxy_acls WHERE id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scan(row interface{ Scan(...any) error }) (*models.ForwardProxyACL, error) {
|
||||||
|
var a models.ForwardProxyACL
|
||||||
|
if err := row.Scan(
|
||||||
|
&a.ID, &a.Name, &a.ACLType, &a.Value, &a.Action,
|
||||||
|
&a.Priority, &a.Active, &a.Comment,
|
||||||
|
&a.CreatedAt, &a.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
62
internal/squid/squid.cfg.tpl
Normal file
62
internal/squid/squid.cfg.tpl
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Generated by edgeguard — do not edit by hand.
|
||||||
|
# Source: internal/squid/squid.go (template: squid.cfg.tpl).
|
||||||
|
# Re-generate via `edgeguard-ctl render-config --only=squid`.
|
||||||
|
|
||||||
|
http_port {{.ListenPort}}
|
||||||
|
|
||||||
|
# Standard cache directory + small in-memory cache. Forward proxy
|
||||||
|
# isn't a CDN — we keep cache modest to avoid disk pressure.
|
||||||
|
cache_dir ufs /var/spool/squid 100 16 256
|
||||||
|
cache_mem 64 MB
|
||||||
|
|
||||||
|
# Logging — combined access log, rotated by logrotate.
|
||||||
|
access_log /var/log/squid/access.log squid
|
||||||
|
cache_log /var/log/squid/cache.log
|
||||||
|
|
||||||
|
# Standard safe defaults.
|
||||||
|
acl localnet src 10.0.0.0/8
|
||||||
|
acl localnet src 172.16.0.0/12
|
||||||
|
acl localnet src 192.168.0.0/16
|
||||||
|
acl localnet src fc00::/7
|
||||||
|
acl localnet src fe80::/10
|
||||||
|
|
||||||
|
acl SSL_ports port 443
|
||||||
|
acl Safe_ports port 80 # http
|
||||||
|
acl Safe_ports port 21 # ftp
|
||||||
|
acl Safe_ports port 443 # https
|
||||||
|
acl Safe_ports port 70 # gopher
|
||||||
|
acl Safe_ports port 210 # wais
|
||||||
|
acl Safe_ports port 1025-65535 # unregistered
|
||||||
|
acl Safe_ports port 280 # http-mgmt
|
||||||
|
acl Safe_ports port 488 # gss-http
|
||||||
|
acl Safe_ports port 591 # filemaker
|
||||||
|
acl Safe_ports port 777 # multiling http
|
||||||
|
acl CONNECT method CONNECT
|
||||||
|
|
||||||
|
# Operator-defined ACLs from /api/v1/forward-proxy-acls. Order is
|
||||||
|
# priority desc — first http_access match wins.
|
||||||
|
{{- range .ACLs}}
|
||||||
|
{{- if .Active}}
|
||||||
|
# {{if .Comment}}{{.Comment}}{{else}}{{.Name}} (priority {{.Priority}}){{end}}
|
||||||
|
acl {{.Name}} {{.ACLType}} {{.Value}}
|
||||||
|
http_access {{.Action}} {{.Name}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
# Built-in safety rules — same as squid's default; placed after
|
||||||
|
# operator-rules so they act as fallbacks, not overrides.
|
||||||
|
http_access deny !Safe_ports
|
||||||
|
http_access deny CONNECT !SSL_ports
|
||||||
|
http_access allow localhost manager
|
||||||
|
http_access deny manager
|
||||||
|
http_access allow localhost
|
||||||
|
# Default-policy: only localnet may use the proxy if no operator-rule
|
||||||
|
# explicitly allowed/denied. Stricter than squid's default to keep
|
||||||
|
# the proxy from becoming an open relay.
|
||||||
|
http_access allow localnet
|
||||||
|
http_access deny all
|
||||||
|
|
||||||
|
# Hostnames + visible name — operator can override via squid.conf
|
||||||
|
# drop-in if needed.
|
||||||
|
visible_hostname edgeguard-proxy
|
||||||
|
forwarded_for on
|
||||||
@@ -1,20 +1,94 @@
|
|||||||
// Package squid will render /etc/edgeguard/squid/squid.conf in
|
// Package squid renders /etc/edgeguard/squid/squid.conf from
|
||||||
// Phase 3. v1 ships a stub returning configgen.ErrNotImplemented so
|
// forward_proxy_acls and reloads squid.service. Cache directory
|
||||||
// the orchestrator can list it without crashing.
|
// + in-memory cache are hard-coded sensible defaults; the operator
|
||||||
|
// scope is just "what can pass through the proxy".
|
||||||
package squid
|
package squid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/configgen"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/configgen"
|
||||||
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||||
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/forwardproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Generator struct{}
|
const (
|
||||||
|
confPath = "/etc/edgeguard/squid/squid.conf"
|
||||||
|
listenPort = 3128
|
||||||
|
)
|
||||||
|
|
||||||
func New() *Generator { return &Generator{} }
|
//go:embed squid.cfg.tpl
|
||||||
|
var cfgTpl string
|
||||||
|
|
||||||
|
var tpl = template.Must(template.New("squid").Parse(cfgTpl))
|
||||||
|
|
||||||
|
type View struct {
|
||||||
|
ListenPort int
|
||||||
|
ACLs []models.ForwardProxyACL
|
||||||
|
}
|
||||||
|
|
||||||
|
type Generator struct {
|
||||||
|
Pool *pgxpool.Pool
|
||||||
|
Repo *forwardproxy.Repo
|
||||||
|
SkipReload bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(pool *pgxpool.Pool) *Generator {
|
||||||
|
return &Generator{Pool: pool, Repo: forwardproxy.New(pool)}
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Generator) Name() string { return "squid" }
|
func (g *Generator) Name() string { return "squid" }
|
||||||
|
|
||||||
func (g *Generator) Render(ctx context.Context) error {
|
func (g *Generator) Render(ctx context.Context) error {
|
||||||
return configgen.ErrNotImplemented
|
acls, err := g.Repo.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list acls: %w", err)
|
||||||
|
}
|
||||||
|
view := View{ListenPort: listenPort, ACLs: acls}
|
||||||
|
var body bytes.Buffer
|
||||||
|
if err := tpl.Execute(&body, view); err != nil {
|
||||||
|
return fmt.Errorf("template: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(confPath), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("mkdir: %w", err)
|
||||||
|
}
|
||||||
|
tmp := confPath + ".tmp"
|
||||||
|
if err := os.WriteFile(tmp, body.Bytes(), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write %s: %w", tmp, err)
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmp, confPath); err != nil {
|
||||||
|
return fmt.Errorf("rename: %w", err)
|
||||||
|
}
|
||||||
|
if err := ensureDistroSymlink(); err != nil {
|
||||||
|
return fmt.Errorf("symlink: %w", err)
|
||||||
|
}
|
||||||
|
if g.SkipReload {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return configgen.ReloadService("squid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDistroSymlink legt /etc/squid/squid.conf als Symlink auf
|
||||||
|
// unsere managed conf an. Squid systemd-Unit liest die Distro-Datei;
|
||||||
|
// ohne Symlink driftet der edgeguard-Renderer und der laufende
|
||||||
|
// Daemon auseinander (gleicher Bug-Pattern wie wg-quick).
|
||||||
|
// Existing real file (Distro-Default) wird nach .distro-bak verschoben,
|
||||||
|
// nicht gelöscht.
|
||||||
|
func ensureDistroSymlink() error {
|
||||||
|
const link = "/etc/squid/squid.conf"
|
||||||
|
if cur, err := os.Readlink(link); err == nil && cur == confPath {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(link); err == nil {
|
||||||
|
_ = os.Rename(link, link+".distro-bak")
|
||||||
|
}
|
||||||
|
return os.Symlink(confPath, link)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "edgeguard-management-ui",
|
"name": "edgeguard-management-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.25",
|
"version": "1.0.26",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const IPAddressesPage = lazy(() => import('./pages/IPAddresses'))
|
|||||||
const SSLPage = lazy(() => import('./pages/SSL'))
|
const SSLPage = lazy(() => import('./pages/SSL'))
|
||||||
const FirewallPage = lazy(() => import('./pages/Firewall'))
|
const FirewallPage = lazy(() => import('./pages/Firewall'))
|
||||||
const WireguardPage = lazy(() => import('./pages/Wireguard'))
|
const WireguardPage = lazy(() => import('./pages/Wireguard'))
|
||||||
|
const ForwardProxyPage = lazy(() => import('./pages/ForwardProxy'))
|
||||||
const ClusterPage = lazy(() => import('./pages/Cluster'))
|
const ClusterPage = lazy(() => import('./pages/Cluster'))
|
||||||
const SettingsPage = lazy(() => import('./pages/Settings'))
|
const SettingsPage = lazy(() => import('./pages/Settings'))
|
||||||
|
|
||||||
@@ -101,6 +102,7 @@ export default function App() {
|
|||||||
<Route path="/ssl" element={<SSLPage />} />
|
<Route path="/ssl" element={<SSLPage />} />
|
||||||
<Route path="/firewall" element={<FirewallPage />} />
|
<Route path="/firewall" element={<FirewallPage />} />
|
||||||
<Route path="/vpn/wireguard" element={<WireguardPage />} />
|
<Route path="/vpn/wireguard" element={<WireguardPage />} />
|
||||||
|
<Route path="/forward-proxy" element={<ForwardProxyPage />} />
|
||||||
<Route path="/cluster" element={<ClusterPage />} />
|
<Route path="/cluster" element={<ClusterPage />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NavLink } from 'react-router-dom'
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
ApartmentOutlined,
|
ApartmentOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
ClusterOutlined,
|
ClusterOutlined,
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
DatabaseOutlined,
|
DatabaseOutlined,
|
||||||
@@ -57,8 +58,9 @@ const NAV: NavSection[] = [
|
|||||||
{
|
{
|
||||||
labelKey: 'nav.section.security',
|
labelKey: 'nav.section.security',
|
||||||
items: [
|
items: [
|
||||||
{ path: '/firewall', labelKey: 'nav.firewall', icon: <FireOutlined /> },
|
{ path: '/firewall', labelKey: 'nav.firewall', icon: <FireOutlined /> },
|
||||||
{ path: '/vpn/wireguard', labelKey: 'nav.wireguard', icon: <ThunderboltOutlined /> },
|
{ path: '/vpn/wireguard', labelKey: 'nav.wireguard', icon: <ThunderboltOutlined /> },
|
||||||
|
{ path: '/forward-proxy', labelKey: 'nav.forwardProxy', icon: <CloudServerOutlined /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,7 +72,7 @@ const NAV: NavSection[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const VERSION = '1.0.25'
|
const VERSION = '1.0.26'
|
||||||
|
|
||||||
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
|
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"ssl": "SSL-Zertifikate",
|
"ssl": "SSL-Zertifikate",
|
||||||
"vpn": "VPN",
|
"vpn": "VPN",
|
||||||
"wireguard": "WireGuard",
|
"wireguard": "WireGuard",
|
||||||
|
"forwardProxy": "Forward-Proxy",
|
||||||
"firewall": "Firewall",
|
"firewall": "Firewall",
|
||||||
"cluster": "Cluster",
|
"cluster": "Cluster",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
@@ -398,6 +399,25 @@
|
|||||||
"wg": "WireGuard"
|
"wg": "WireGuard"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fwd": {
|
||||||
|
"title": "Forward-Proxy (Squid)",
|
||||||
|
"intro": "Squid-basierter Forward-Proxy auf :3128. ACLs werden top-down nach Priority ausgewertet — first-match wins. Wenn keine Regel passt, gewinnt der Default: nur localnet (10/8, 172.16/12, 192.168/16) darf raus.",
|
||||||
|
"helpTitle": "Tipp zur ACL-Reihenfolge",
|
||||||
|
"helpBody": "Höhere Priority = wird zuerst geprüft. Beispiel: 'deny .badsite.com' (priority 200) vor 'allow .com' (priority 100). Werte können Listen sein (mehrere Zeilen), Regex je nach acl_type.",
|
||||||
|
"name": "Name",
|
||||||
|
"nameExtra": "Squid-konformer Bezeichner — Kleinbuchstaben + _, kein Leerzeichen.",
|
||||||
|
"aclType": "Typ",
|
||||||
|
"aclTypeExtra": "Was Squid prüft (Quelle, Domain, Port, …).",
|
||||||
|
"value": "Wert",
|
||||||
|
"valueExtra": "Format hängt vom Typ ab — IPs/CIDRs für src/dst, Domain mit führendem . für dstdomain (.example.com matcht auch sub.example.com), Regex für *_regex-Typen.",
|
||||||
|
"action": "Aktion",
|
||||||
|
"priority": "Priority",
|
||||||
|
"priorityExtra": "Höher = wird zuerst geprüft.",
|
||||||
|
"comment": "Kommentar",
|
||||||
|
"add": "ACL hinzufügen",
|
||||||
|
"edit": "ACL bearbeiten",
|
||||||
|
"deleteConfirm": "ACL {{name}} wirklich löschen?"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"no": "Nein",
|
"no": "Nein",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"ssl": "SSL certificates",
|
"ssl": "SSL certificates",
|
||||||
"vpn": "VPN",
|
"vpn": "VPN",
|
||||||
"wireguard": "WireGuard",
|
"wireguard": "WireGuard",
|
||||||
|
"forwardProxy": "Forward proxy",
|
||||||
"firewall": "Firewall",
|
"firewall": "Firewall",
|
||||||
"cluster": "Cluster",
|
"cluster": "Cluster",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
@@ -398,6 +399,25 @@
|
|||||||
"wg": "WireGuard"
|
"wg": "WireGuard"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fwd": {
|
||||||
|
"title": "Forward proxy (Squid)",
|
||||||
|
"intro": "Squid-based forward proxy on :3128. ACLs are evaluated top-down by priority — first match wins. If no rule matches, the default permits only localnet (10/8, 172.16/12, 192.168/16).",
|
||||||
|
"helpTitle": "ACL ordering tip",
|
||||||
|
"helpBody": "Higher priority = evaluated first. Example: 'deny .badsite.com' (priority 200) before 'allow .com' (priority 100). Values can be lists (multiple lines), regex depending on acl_type.",
|
||||||
|
"name": "Name",
|
||||||
|
"nameExtra": "Squid-conformant identifier — lowercase + _, no spaces.",
|
||||||
|
"aclType": "Type",
|
||||||
|
"aclTypeExtra": "What Squid matches (source, domain, port, …).",
|
||||||
|
"value": "Value",
|
||||||
|
"valueExtra": "Format depends on type — IPs/CIDRs for src/dst, domain with leading dot for dstdomain (.example.com also matches sub.example.com), regex for *_regex types.",
|
||||||
|
"action": "Action",
|
||||||
|
"priority": "Priority",
|
||||||
|
"priorityExtra": "Higher = evaluated first.",
|
||||||
|
"comment": "Comment",
|
||||||
|
"add": "Add ACL",
|
||||||
|
"edit": "Edit ACL",
|
||||||
|
"deleteConfirm": "Really delete ACL {{name}}?"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
|
|||||||
198
management-ui/src/pages/ForwardProxy/index.tsx
Normal file
198
management-ui/src/pages/ForwardProxy/index.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import {
|
||||||
|
Alert, Button, Form, Input, InputNumber, Modal, Select, Switch, Tag, Typography, message,
|
||||||
|
} from 'antd'
|
||||||
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
|
import { CloudServerOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import DataTable from '../../components/DataTable'
|
||||||
|
import PageHeader from '../../components/PageHeader'
|
||||||
|
import ActionButtons from '../../components/ActionButtons'
|
||||||
|
import StatusDot from '../../components/StatusDot'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
|
||||||
|
interface ACL {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
acl_type: string
|
||||||
|
value: string
|
||||||
|
action: 'allow' | 'deny'
|
||||||
|
priority: number
|
||||||
|
active: boolean
|
||||||
|
comment?: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
name: string
|
||||||
|
acl_type: string
|
||||||
|
value: string
|
||||||
|
action: 'allow' | 'deny'
|
||||||
|
priority: number
|
||||||
|
active: boolean
|
||||||
|
comment?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACL-Typen aus Squid's Vokabular — dieselbe Whitelist wie der
|
||||||
|
// Backend-Validator. Mehr Typen kann Squid (browser, time-of-day,
|
||||||
|
// arp, ...) — die spielen wir später dazu.
|
||||||
|
const ACL_TYPES = [
|
||||||
|
{ value: 'src', label: 'src — Quell-IP/CIDR' },
|
||||||
|
{ value: 'dst', label: 'dst — Ziel-IP/CIDR' },
|
||||||
|
{ value: 'dstdomain', label: 'dstdomain — Ziel-Domain (exact)' },
|
||||||
|
{ value: 'srcdomain', label: 'srcdomain — Quell-Domain (rDNS)' },
|
||||||
|
{ value: 'port', label: 'port — Ziel-Port' },
|
||||||
|
{ value: 'proto', label: 'proto — http/https/ftp/...' },
|
||||||
|
{ value: 'method', label: 'method — GET/POST/CONNECT/...' },
|
||||||
|
{ value: 'time', label: 'time — Wochentag/Zeit' },
|
||||||
|
{ value: 'url_regex', label: 'url_regex — kompletter URL-Match' },
|
||||||
|
{ value: 'urlpath_regex', label: 'urlpath_regex — Path-Teil' },
|
||||||
|
{ value: 'dstdom_regex', label: 'dstdom_regex — Domain-Regex' },
|
||||||
|
{ value: 'srcdom_regex', label: 'srcdom_regex — Quell-Domain-Regex' },
|
||||||
|
{ value: 'browser', label: 'browser — User-Agent-Regex' },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function listACLs(): Promise<ACL[]> {
|
||||||
|
const r = await apiClient.get('/forward-proxy/acls')
|
||||||
|
if (!isEnvelope(r.data)) return []
|
||||||
|
return (r.data.data as { acls?: ACL[] }).acls ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ForwardProxyPage() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const { data, isLoading } = useQuery({ queryKey: ['fwd-proxy', 'acls'], queryFn: listACLs })
|
||||||
|
|
||||||
|
const [editing, setEditing] = useState<ACL | null>(null)
|
||||||
|
const [creating, setCreating] = useState(false)
|
||||||
|
const [form] = Form.useForm<FormValues>()
|
||||||
|
|
||||||
|
const upsert = useMutation({
|
||||||
|
mutationFn: async (v: FormValues) => {
|
||||||
|
if (editing) return (await apiClient.put(`/forward-proxy/acls/${editing.id}`, v)).data
|
||||||
|
return (await apiClient.post('/forward-proxy/acls', v)).data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success(t('common.save'))
|
||||||
|
setEditing(null); setCreating(false); form.resetFields()
|
||||||
|
void qc.invalidateQueries({ queryKey: ['fwd-proxy', 'acls'] })
|
||||||
|
},
|
||||||
|
onError: (e: Error) => message.error(e.message),
|
||||||
|
})
|
||||||
|
const del = useMutation({
|
||||||
|
mutationFn: async (id: number) => { await apiClient.delete(`/forward-proxy/acls/${id}`) },
|
||||||
|
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fwd-proxy', 'acls'] }) },
|
||||||
|
onError: (e: Error) => message.error(e.message),
|
||||||
|
})
|
||||||
|
|
||||||
|
const cols: ColumnsType<ACL> = [
|
||||||
|
{ title: t('fwd.priority'), dataIndex: 'priority', key: 'priority', width: 90 },
|
||||||
|
{ title: t('fwd.name'), dataIndex: 'name', key: 'name', render: (s: string) => <code>{s}</code> },
|
||||||
|
{ title: t('fwd.action'), dataIndex: 'action', key: 'action',
|
||||||
|
render: (a: string) => <Tag color={a === 'allow' ? 'green' : 'red'}>{a.toUpperCase()}</Tag> },
|
||||||
|
{ title: t('fwd.aclType'), dataIndex: 'acl_type', key: 'acl_type',
|
||||||
|
render: (s: string) => <code>{s}</code> },
|
||||||
|
{ title: t('fwd.value'), dataIndex: 'value', key: 'value',
|
||||||
|
render: (s: string) => <Text code style={{ fontSize: 12 }}>{s}</Text> },
|
||||||
|
{ title: t('fwd.comment'), dataIndex: 'comment', key: 'comment',
|
||||||
|
render: (v?: string | null) => v ?? '—' },
|
||||||
|
{ title: t('common.active'), dataIndex: 'active', key: 'active',
|
||||||
|
render: (v: boolean) => <StatusDot active={v} /> },
|
||||||
|
{
|
||||||
|
title: t('common.actions'), key: 'actions',
|
||||||
|
render: (_, row) => (
|
||||||
|
<ActionButtons
|
||||||
|
onEdit={() => {
|
||||||
|
setEditing(row)
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: row.name, acl_type: row.acl_type, value: row.value,
|
||||||
|
action: row.action, priority: row.priority, active: row.active,
|
||||||
|
comment: row.comment ?? undefined,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onDelete={() => del.mutate(row.id)}
|
||||||
|
deleteConfirm={t('fwd.deleteConfirm', { name: row.name })}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
icon={<CloudServerOutlined />}
|
||||||
|
title={t('fwd.title')}
|
||||||
|
subtitle={t('fwd.intro')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
className="mb-12"
|
||||||
|
message={t('fwd.helpTitle')}
|
||||||
|
description={t('fwd.helpBody')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
rowKey="id"
|
||||||
|
loading={isLoading}
|
||||||
|
dataSource={data ?? []}
|
||||||
|
columns={cols}
|
||||||
|
extraActions={
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => {
|
||||||
|
setCreating(true); form.resetFields()
|
||||||
|
form.setFieldsValue({ priority: 100, active: true, action: 'allow', acl_type: 'dstdomain' })
|
||||||
|
}}>
|
||||||
|
{t('fwd.add')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={editing ? t('fwd.edit') : t('fwd.add')}
|
||||||
|
open={editing !== null || creating}
|
||||||
|
onCancel={() => { setEditing(null); setCreating(false); form.resetFields() }}
|
||||||
|
onOk={() => { void form.submit() }}
|
||||||
|
confirmLoading={upsert.isPending}
|
||||||
|
width={620}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" onFinish={(v) => upsert.mutate(v)}>
|
||||||
|
<Form.Item label={t('fwd.name')} name="name" rules={[{ required: true }]}
|
||||||
|
extra={t('fwd.nameExtra')}>
|
||||||
|
<Input placeholder="allow_internal_lan" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('fwd.action')} name="action" rules={[{ required: true }]}>
|
||||||
|
<Select options={[
|
||||||
|
{ value: 'allow', label: 'allow — Zugriff erlauben' },
|
||||||
|
{ value: 'deny', label: 'deny — Zugriff blockieren' },
|
||||||
|
]} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('fwd.aclType')} name="acl_type" rules={[{ required: true }]}
|
||||||
|
extra={t('fwd.aclTypeExtra')}>
|
||||||
|
<Select options={ACL_TYPES} showSearch optionFilterProp="value" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('fwd.value')} name="value" rules={[{ required: true }]}
|
||||||
|
extra={t('fwd.valueExtra')}>
|
||||||
|
<Input.TextArea rows={2} placeholder=".example.com oder 10.10.20.0/24 oder 443" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('fwd.priority')} name="priority" rules={[{ required: true }]}
|
||||||
|
extra={t('fwd.priorityExtra')}>
|
||||||
|
<InputNumber min={0} max={1000} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('fwd.comment')} name="comment">
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('common.active')} name="active" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -47,6 +47,8 @@ edgeguard ALL=(root) NOPASSWD: /bin/systemctl restart wg-quick@*.service
|
|||||||
edgeguard ALL=(root) NOPASSWD: /bin/systemctl stop wg-quick@*.service
|
edgeguard ALL=(root) NOPASSWD: /bin/systemctl stop wg-quick@*.service
|
||||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg show all dump
|
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg show all dump
|
||||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg show *
|
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg show *
|
||||||
|
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reload squid.service
|
||||||
|
edgeguard ALL=(root) NOPASSWD: /bin/systemctl reload squid.service
|
||||||
SUDOERS
|
SUDOERS
|
||||||
chmod 0440 /etc/sudoers.d/edgeguard
|
chmod 0440 /etc/sudoers.d/edgeguard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user