feat!: Migration auf Go-Binary
BREAKING CHANGE: Die alte Shell-Version muss vor der Installation der Go-Version deinstalliert werden.
This commit is contained in:
203
internal/firewall/firewall.go
Normal file
203
internal/firewall/firewall.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Executor interface {
|
||||
Run(ctx context.Context, name string, args ...string) error
|
||||
}
|
||||
|
||||
type OSExecutor struct{}
|
||||
|
||||
func (OSExecutor) Run(ctx context.Context, name string, args ...string) error {
|
||||
return exec.CommandContext(ctx, name, args...).Run()
|
||||
}
|
||||
|
||||
type Firewall struct {
|
||||
Exec Executor
|
||||
Chain string
|
||||
Ports []string
|
||||
Mode string
|
||||
DryRun bool
|
||||
Set4 string
|
||||
Set6 string
|
||||
}
|
||||
|
||||
func New(exec Executor, chain string, ports []string, mode string, dry bool) *Firewall {
|
||||
return &Firewall{Exec: exec, Chain: chain, Ports: ports, Mode: normalizeMode(mode), DryRun: dry, Set4: "adguard_shield_v4", Set6: "adguard_shield_v6"}
|
||||
}
|
||||
|
||||
func (f *Firewall) Setup(ctx context.Context) error {
|
||||
if f.DryRun {
|
||||
return nil
|
||||
}
|
||||
if len(f.hooks("iptables")) == 0 {
|
||||
return fmt.Errorf("unsupported firewall mode %q", f.Mode)
|
||||
}
|
||||
_ = f.Exec.Run(ctx, "ipset", "create", f.Set4, "hash:net", "family", "inet", "timeout", "0", "-exist")
|
||||
_ = f.Exec.Run(ctx, "ipset", "create", f.Set6, "hash:net", "family", "inet6", "timeout", "0", "-exist")
|
||||
_ = f.Exec.Run(ctx, "iptables", "-N", f.Chain)
|
||||
_ = f.Exec.Run(ctx, "ip6tables", "-N", f.Chain)
|
||||
if err := ensureSetDrop(ctx, f.Exec, "iptables", f.Chain, f.Set4); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureSetDrop(ctx, f.Exec, "ip6tables", f.Chain, f.Set6); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.ensureHooks(ctx, "iptables"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.ensureHooks(ctx, "ip6tables"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureRule(ctx context.Context, ex Executor, bin string, args ...string) bool {
|
||||
return ex.Run(ctx, bin, args...) == nil
|
||||
}
|
||||
|
||||
func ensureSetDrop(ctx context.Context, ex Executor, bin, chain, set string) error {
|
||||
check := []string{"-C", chain, "-m", "set", "--match-set", set, "src", "-j", "DROP"}
|
||||
if ex.Run(ctx, bin, check...) == nil {
|
||||
return nil
|
||||
}
|
||||
return ex.Run(ctx, bin, "-I", chain, "-m", "set", "--match-set", set, "src", "-j", "DROP")
|
||||
}
|
||||
|
||||
type hook struct {
|
||||
Chain string
|
||||
OptionalMissing bool
|
||||
}
|
||||
|
||||
func normalizeMode(mode string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||
case "", "host", "classic", "native", "docker-host":
|
||||
return "host"
|
||||
case "docker", "docker-bridge", "docker-published", "published":
|
||||
return "docker-bridge"
|
||||
case "hybrid", "both":
|
||||
return "hybrid"
|
||||
default:
|
||||
return strings.ToLower(strings.TrimSpace(mode))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Firewall) hooks(bin string) []hook {
|
||||
docker := hook{Chain: "DOCKER-USER", OptionalMissing: bin == "ip6tables"}
|
||||
switch f.Mode {
|
||||
case "host":
|
||||
return []hook{{Chain: "INPUT"}}
|
||||
case "docker-bridge":
|
||||
return []hook{docker}
|
||||
case "hybrid":
|
||||
return []hook{{Chain: "INPUT"}, docker}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Firewall) ensureHooks(ctx context.Context, bin string) error {
|
||||
for _, h := range f.hooks(bin) {
|
||||
if !chainExists(ctx, f.Exec, bin, h.Chain) {
|
||||
if h.OptionalMissing {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("%s chain %s not found", bin, h.Chain)
|
||||
}
|
||||
for _, p := range f.Ports {
|
||||
for _, proto := range []string{"tcp", "udp"} {
|
||||
check := []string{"-C", h.Chain, "-p", proto, "--dport", p, "-j", f.Chain}
|
||||
if ensureRule(ctx, f.Exec, bin, check...) {
|
||||
continue
|
||||
}
|
||||
_ = f.Exec.Run(ctx, bin, "-I", h.Chain, "-p", proto, "--dport", p, "-j", f.Chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func chainExists(ctx context.Context, ex Executor, bin, chain string) bool {
|
||||
return ex.Run(ctx, bin, "-n", "-L", chain) == nil
|
||||
}
|
||||
|
||||
func (f *Firewall) Add(ctx context.Context, ip string, timeout int64) error {
|
||||
if f.DryRun {
|
||||
return nil
|
||||
}
|
||||
set, err := f.setFor(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args := []string{"add", set, ip, "-exist"}
|
||||
if timeout > 0 {
|
||||
args = append(args, "timeout", strconv.FormatInt(timeout, 10))
|
||||
}
|
||||
return f.Exec.Run(ctx, "ipset", args...)
|
||||
}
|
||||
|
||||
func (f *Firewall) Del(ctx context.Context, ip string) error {
|
||||
if f.DryRun {
|
||||
return nil
|
||||
}
|
||||
set, err := f.setFor(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = f.Exec.Run(ctx, "ipset", "del", set, ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewall) Flush(ctx context.Context) error {
|
||||
if f.DryRun {
|
||||
return nil
|
||||
}
|
||||
_ = f.Exec.Run(ctx, "ipset", "flush", f.Set4)
|
||||
_ = f.Exec.Run(ctx, "ipset", "flush", f.Set6)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewall) Remove(ctx context.Context) error {
|
||||
if f.DryRun {
|
||||
return nil
|
||||
}
|
||||
for _, p := range f.Ports {
|
||||
for _, proto := range []string{"tcp", "udp"} {
|
||||
for _, parent := range []string{"INPUT", "DOCKER-USER"} {
|
||||
_ = f.Exec.Run(ctx, "iptables", "-D", parent, "-p", proto, "--dport", p, "-j", f.Chain)
|
||||
_ = f.Exec.Run(ctx, "ip6tables", "-D", parent, "-p", proto, "--dport", p, "-j", f.Chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = f.Exec.Run(ctx, "iptables", "-F", f.Chain)
|
||||
_ = f.Exec.Run(ctx, "ip6tables", "-F", f.Chain)
|
||||
_ = f.Exec.Run(ctx, "iptables", "-X", f.Chain)
|
||||
_ = f.Exec.Run(ctx, "ip6tables", "-X", f.Chain)
|
||||
_ = f.Exec.Run(ctx, "ipset", "destroy", f.Set4)
|
||||
_ = f.Exec.Run(ctx, "ipset", "destroy", f.Set6)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewall) setFor(s string) (string, error) {
|
||||
if p, err := netip.ParsePrefix(s); err == nil {
|
||||
if p.Addr().Is4() {
|
||||
return f.Set4, nil
|
||||
}
|
||||
return f.Set6, nil
|
||||
}
|
||||
a, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid IP/prefix %q", s)
|
||||
}
|
||||
if a.Is4() {
|
||||
return f.Set4, nil
|
||||
}
|
||||
return f.Set6, nil
|
||||
}
|
||||
142
internal/firewall/firewall_test.go
Normal file
142
internal/firewall/firewall_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeExec struct {
|
||||
calls []string
|
||||
failChecks bool
|
||||
missing map[string]bool
|
||||
}
|
||||
|
||||
func (f *fakeExec) Run(_ context.Context, name string, args ...string) error {
|
||||
call := name + " " + strings.Join(args, " ")
|
||||
f.calls = append(f.calls, call)
|
||||
if f.missing != nil && f.missing[call] {
|
||||
return errFake
|
||||
}
|
||||
if f.failChecks && len(args) > 0 && args[0] == "-C" {
|
||||
return errFake
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeErr string
|
||||
|
||||
func (e fakeErr) Error() string { return string(e) }
|
||||
|
||||
var errFake = fakeErr("missing")
|
||||
|
||||
func TestFirewallSetupCreatesSetsAndRules(t *testing.T) {
|
||||
ex := &fakeExec{failChecks: true}
|
||||
fw := New(ex, "ADGUARD_SHIELD", []string{"53"}, "host", false)
|
||||
if err := fw.Setup(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joined := strings.Join(ex.calls, "\n")
|
||||
for _, want := range []string{
|
||||
"ipset create adguard_shield_v4 hash:net family inet timeout 0 -exist",
|
||||
"iptables -I INPUT -p tcp --dport 53 -j ADGUARD_SHIELD",
|
||||
"iptables -I ADGUARD_SHIELD -m set --match-set adguard_shield_v4 src -j DROP",
|
||||
"ip6tables -I ADGUARD_SHIELD -m set --match-set adguard_shield_v6 src -j DROP",
|
||||
} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Fatalf("missing call %q in:\n%s", want, joined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallSetupUsesDockerUserForBridgeMode(t *testing.T) {
|
||||
ex := &fakeExec{failChecks: true}
|
||||
fw := New(ex, "ADGUARD_SHIELD", []string{"53"}, "docker-bridge", false)
|
||||
if err := fw.Setup(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joined := strings.Join(ex.calls, "\n")
|
||||
if !strings.Contains(joined, "iptables -I DOCKER-USER -p udp --dport 53 -j ADGUARD_SHIELD") {
|
||||
t.Fatalf("missing docker hook in:\n%s", joined)
|
||||
}
|
||||
if strings.Contains(joined, "iptables -I INPUT -p udp --dport 53 -j ADGUARD_SHIELD") {
|
||||
t.Fatalf("unexpected INPUT hook in docker-bridge mode:\n%s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallSetupHybridUsesInputAndDockerUser(t *testing.T) {
|
||||
ex := &fakeExec{failChecks: true}
|
||||
fw := New(ex, "ADGUARD_SHIELD", []string{"53"}, "hybrid", false)
|
||||
if err := fw.Setup(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joined := strings.Join(ex.calls, "\n")
|
||||
for _, want := range []string{
|
||||
"iptables -I INPUT -p tcp --dport 53 -j ADGUARD_SHIELD",
|
||||
"iptables -I DOCKER-USER -p tcp --dport 53 -j ADGUARD_SHIELD",
|
||||
} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Fatalf("missing call %q in:\n%s", want, joined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallSetupRequiresDockerUserForIPv4BridgeMode(t *testing.T) {
|
||||
ex := &fakeExec{missing: map[string]bool{"iptables -n -L DOCKER-USER": true}}
|
||||
fw := New(ex, "ADGUARD_SHIELD", []string{"53"}, "docker-bridge", false)
|
||||
if err := fw.Setup(context.Background()); err == nil || !strings.Contains(err.Error(), "DOCKER-USER") {
|
||||
t.Fatalf("expected DOCKER-USER error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallSetupSkipsMissingIPv6DockerUser(t *testing.T) {
|
||||
ex := &fakeExec{
|
||||
failChecks: true,
|
||||
missing: map[string]bool{"ip6tables -n -L DOCKER-USER": true},
|
||||
}
|
||||
fw := New(ex, "ADGUARD_SHIELD", []string{"53"}, "docker-bridge", false)
|
||||
if err := fw.Setup(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joined := strings.Join(ex.calls, "\n")
|
||||
if strings.Contains(joined, "ip6tables -I DOCKER-USER") {
|
||||
t.Fatalf("unexpected IPv6 docker hook with missing DOCKER-USER:\n%s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallSetupRejectsUnknownMode(t *testing.T) {
|
||||
fw := New(&fakeExec{}, "ADGUARD_SHIELD", []string{"53"}, "surprise", false)
|
||||
err := fw.Setup(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported firewall mode") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallAddChoosesFamily(t *testing.T) {
|
||||
ex := &fakeExec{}
|
||||
fw := New(ex, "ADGUARD_SHIELD", []string{"53"}, "host", false)
|
||||
if err := fw.Add(context.Background(), "2001:db8::1", 30); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := strings.Join(ex.calls, "\n")
|
||||
if !strings.Contains(got, "ipset add adguard_shield_v6 2001:db8::1 -exist timeout 30") {
|
||||
t.Fatalf("unexpected calls:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallRemoveDeletesAllKnownHooks(t *testing.T) {
|
||||
ex := &fakeExec{}
|
||||
fw := New(ex, "ADGUARD_SHIELD", []string{"53"}, "host", false)
|
||||
if err := fw.Remove(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joined := strings.Join(ex.calls, "\n")
|
||||
for _, want := range []string{
|
||||
"iptables -D INPUT -p tcp --dport 53 -j ADGUARD_SHIELD",
|
||||
"iptables -D DOCKER-USER -p tcp --dport 53 -j ADGUARD_SHIELD",
|
||||
} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Fatalf("missing cleanup call %q in:\n%s", want, joined)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user