Firewall added & some fixes

This commit is contained in:
MoonDev
2026-04-13 12:40:49 +03:00
parent 7eaa9750b0
commit 8c35022483
22 changed files with 1659 additions and 134 deletions

View File

@@ -49,7 +49,9 @@ func HandleInterfaces(w http.ResponseWriter, r *http.Request) {
}
result := make([]iface, 0, len(names))
existingNames := map[string]bool{}
for _, name := range names {
existingNames[name] = true
s, err := network.GetInterfaceStats(name)
if err != nil {
continue
@@ -61,6 +63,25 @@ func HandleInterfaces(w http.ResponseWriter, r *http.Request) {
result = append(result, iface{s, hasPending})
}
// Also include pending VLAN configs not yet present in the system.
for name, cfg := range network.GetAllPending() {
if existingNames[name] || !network.IsVLAN(name) {
continue
}
s := &network.InterfaceStats{
Name: name,
State: "unknown",
Mode: cfg.Mode,
IPv6: []string{},
}
if cfg.Mode == "static" {
s.IPv4 = cfg.Address
s.IPv4Mask = cfg.Netmask
s.Gateway = cfg.Gateway
}
result = append(result, iface{s, true})
}
ok(w, result)
}
@@ -100,6 +121,13 @@ func HandleInterfaceAction(w http.ResponseWriter, r *http.Request) {
err = network.IfDown(name)
case "restart":
err = network.IfRestart(name)
case "delete":
if !network.IsVLAN(name) {
fail(w, http.StatusBadRequest, "delete is only supported for VLAN interfaces")
return
}
network.ClearPendingConfig(name)
err = network.DeleteVLAN(name)
default:
fail(w, http.StatusBadRequest, "unknown action: "+action)
return

View File

@@ -9,7 +9,6 @@ import (
"alpine-router/clients"
"alpine-router/config"
"alpine-router/dhcp"
"alpine-router/nat"
)
func HandleClients(w http.ResponseWriter, r *http.Request) {
@@ -88,37 +87,6 @@ func updateClient(mac, hostname string, blocked bool, staticIP string) error {
return config.Save(cfg)
}
func applyBlockedFirewall() {
if !nat.IsInstalled() {
return
}
cfg, err := config.Load()
if err != nil {
log.Printf("Warning: load config for blocked firewall: %v", err)
return
}
var blockedIPs []string
for _, kd := range cfg.KnownDevices {
if kd.Blocked {
ip := kd.IP
if kd.StaticIP != "" {
ip = kd.StaticIP
}
if ip != "" {
blockedIPs = append(blockedIPs, ip)
}
}
}
natCfg := &nat.Config{Interfaces: cfg.NAT.Interfaces}
if err := nat.ApplyRulesWithBlocked(natCfg, blockedIPs); err != nil {
log.Printf("Warning: apply blocked firewall rules: %v", err)
} else {
log.Printf("Applied firewall rules (%d blocked clients)", len(blockedIPs))
}
}
func applyDHCPStaticBindings() {
if !dhcp.IsInstalled() {

100
handlers/firewall.go Normal file
View File

@@ -0,0 +1,100 @@
package handlers
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"alpine-router/config"
"alpine-router/nat"
"alpine-router/network"
)
func HandleFirewall(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
handleFirewallGet(w, r)
case http.MethodPost:
handleFirewallSave(w, r)
default:
fail(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func handleFirewallGet(w http.ResponseWriter, r *http.Request) {
cfg, err := config.Load()
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
// Collect interface names for the UI dropdowns.
names, _ := network.GetInterfaces()
ok(w, map[string]interface{}{
"installed": nat.IsInstalled(),
"rules": cfg.Firewall.Rules,
"vlan_isolation": cfg.Firewall.VLANIsolation,
"interfaces": names,
})
}
func handleFirewallSave(w http.ResponseWriter, r *http.Request) {
var body struct {
Rules []config.FirewallRule `json:"rules"`
VLANIsolation bool `json:"vlan_isolation"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
fail(w, http.StatusBadRequest, "invalid json: "+err.Error())
return
}
// Assign IDs to new rules that lack one.
for i := range body.Rules {
if body.Rules[i].ID == "" {
body.Rules[i].ID = fmt.Sprintf("%x", rand.Int63())
}
}
cfg, err := config.Load()
if err != nil {
fail(w, http.StatusInternalServerError, "load config: "+err.Error())
return
}
cfg.Firewall.Rules = body.Rules
cfg.Firewall.VLANIsolation = body.VLANIsolation
if err := config.Save(cfg); err != nil {
fail(w, http.StatusInternalServerError, "save config: "+err.Error())
return
}
ok(w, map[string]string{"message": "saved"})
}
func HandleFirewallApply(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fail(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
if !nat.IsInstalled() {
fail(w, http.StatusServiceUnavailable, "nftables (nft) не установлен — выполните: apk add nftables")
return
}
cfg, err := config.Load()
if err != nil {
fail(w, http.StatusInternalServerError, "load config: "+err.Error())
return
}
if err := applyAllRules(cfg); err != nil {
fail(w, http.StatusInternalServerError, "apply: "+err.Error())
return
}
ok(w, map[string]string{"message": "firewall applied"})
}

View File

@@ -62,10 +62,10 @@ func HandleNATSave(w http.ResponseWriter, r *http.Request) {
return
}
if err := nat.ApplyRules(&cfg); err != nil {
if err := applyAllRules(appCfg); err != nil {
fail(w, http.StatusInternalServerError, "apply: "+err.Error())
return
}
ok(w, map[string]string{"message": "nat applied"})
}
}

98
handlers/rules.go Normal file
View File

@@ -0,0 +1,98 @@
package handlers
import (
"log"
"alpine-router/config"
"alpine-router/firewall"
"alpine-router/nat"
"alpine-router/network"
)
// applyAllRules rebuilds the complete nftables ruleset from the current config:
// NAT masquerade + user firewall rules + VLAN isolation + blocked clients.
func applyAllRules(cfg *config.AppConfig) error {
if !nat.IsInstalled() {
return nil
}
// Collect blocked client IPs.
var blockedIPs []string
for _, kd := range cfg.KnownDevices {
if kd.Blocked {
ip := kd.IP
if kd.StaticIP != "" {
ip = kd.StaticIP
}
if ip != "" {
blockedIPs = append(blockedIPs, ip)
}
}
}
// Build the LAN interface set for isolation:
// all NAT interfaces + all VLAN interfaces (active + pending).
// This ensures native interfaces (eth0) and their VLANs (eth0.100) are all
// mutually isolated when VLANIsolation is enabled.
seen := map[string]bool{}
var lanIfaces []string
addLAN := func(name string) {
if name != "" && !seen[name] {
lanIfaces = append(lanIfaces, name)
seen[name] = true
}
}
for _, name := range cfg.NAT.Interfaces {
addLAN(name)
}
names, _ := network.GetInterfaces()
for _, name := range names {
if network.IsVLAN(name) {
addLAN(name)
addLAN(network.VLANParent(name)) // include parent (native VLAN) too
}
}
for name := range network.GetAllPending() {
if network.IsVLAN(name) {
addLAN(name)
addLAN(network.VLANParent(name))
}
}
// Convert config.FirewallRule → firewall.Rule.
fwRules := make([]firewall.Rule, len(cfg.Firewall.Rules))
for i, r := range cfg.Firewall.Rules {
fwRules[i] = firewall.Rule{
ID: r.ID,
Enabled: r.Enabled,
Action: r.Action,
Protocol: r.Protocol,
SrcAddr: r.SrcAddr,
SrcPort: r.SrcPort,
DstAddr: r.DstAddr,
DstPort: r.DstPort,
InIface: r.InIface,
OutIface: r.OutIface,
Comment: r.Comment,
}
}
return firewall.ApplyAll(
firewall.NATConfig{Interfaces: cfg.NAT.Interfaces},
firewall.Config{Rules: fwRules, VLANIsolation: cfg.Firewall.VLANIsolation},
blockedIPs,
lanIfaces,
)
}
// applyBlockedFirewall is the async helper called after client updates.
func applyBlockedFirewall() {
cfg, err := config.Load()
if err != nil {
log.Printf("Warning: load config for firewall: %v", err)
return
}
if err := applyAllRules(cfg); err != nil {
log.Printf("Warning: apply firewall rules: %v", err)
}
}