Skip to content

Commit

Permalink
fix(transparent-proxy): make iptables mode detection more defensive (…
Browse files Browse the repository at this point in the history
…backport of #9776) (#9788)

* fix(transparent-proxy): make iptables mode detection more defensive (#9776)

Signed-off-by: Bart Smykla <[email protected]>
Co-authored-by: Bart Smykla <[email protected]>
  • Loading branch information
kumahq[bot] and bartsmykla authored Apr 2, 2024
1 parent 35e9401 commit 35f57c2
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 104 deletions.
8 changes: 6 additions & 2 deletions pkg/transparentproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,19 @@ func (c Config) ShouldCaptureAllDNS() bool {
// conntrack zone splitting settings are enabled (return false if not), and then
// will verify if there is conntrack iptables extension available to apply
// the DNS conntrack zone splitting iptables rules
func (c Config) ShouldConntrackZoneSplit() bool {
func (c Config) ShouldConntrackZoneSplit(iptablesExecutable string) bool {
if !c.Redirect.DNS.Enabled || !c.Redirect.DNS.ConntrackZoneSplit {
return false
}

if iptablesExecutable == "" {
iptablesExecutable = "iptables"
}

// There are situations where conntrack extension is not present (WSL2)
// instead of failing the whole iptables application, we can log the warning,
// skip conntrack related rules and move forward
if err := exec.Command("iptables", "-m", "conntrack", "--help").Run(); err != nil {
if err := exec.Command(iptablesExecutable, "-m", "conntrack", "--help").Run(); err != nil {
_, _ = fmt.Fprintf(c.RuntimeStderr,
"# [WARNING] error occurred when validating if 'conntrack' iptables "+
"module is present. Rules for DNS conntrack zone "+
Expand Down
150 changes: 92 additions & 58 deletions pkg/transparentproxy/iptables/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bufio"
"context"
"fmt"
"io"
"net"
"os"
"strings"
Expand Down Expand Up @@ -66,7 +65,12 @@ func (t *IPTables) Build(verbose bool) string {
return strings.Join(tables, separator) + "\n"
}

func BuildIPTables(cfg config.Config, dnsServers []string, ipv6 bool) (string, error) {
func BuildIPTables(
cfg config.Config,
dnsServers []string,
ipv6 bool,
iptablesExecutablePath string,
) (string, error) {
cfg = config.MergeConfigWithDefaults(cfg)

loopbackIface, err := getLoopback()
Expand All @@ -80,7 +84,7 @@ func BuildIPTables(cfg config.Config, dnsServers []string, ipv6 bool) (string, e
}

return newIPTables(
buildRawTable(cfg, dnsServers),
buildRawTable(cfg, dnsServers, iptablesExecutablePath),
natTable,
buildMangleTable(cfg),
).Build(cfg.Verbose), nil
Expand All @@ -89,9 +93,9 @@ func BuildIPTables(cfg config.Config, dnsServers []string, ipv6 bool) (string, e
// runtimeOutput is the file (should be os.Stdout by default) where we can dump generated
// rules for used to see and debug if something goes wrong, which can be overwritten
// in tests to not obfuscate the other, more relevant logs
func saveIPTablesRestoreFile(runtimeOutput io.Writer, f *os.File, content string) error {
_, _ = fmt.Fprintln(runtimeOutput, "# writing following contents to rules file: ", f.Name())
_, _ = fmt.Fprintln(runtimeOutput, content)
func (r *restorer) saveIPTablesRestoreFile(f *os.File, content string) error {
fmt.Fprintf(r.cfg.RuntimeStdout, "# writing following contents to rules file: %s\n", f.Name())
fmt.Fprint(r.cfg.RuntimeStdout, content)

writer := bufio.NewWriter(f)
_, err := writer.WriteString(content)
Expand All @@ -118,83 +122,103 @@ func createRulesFile(ipv6 bool) (*os.File, error) {
return f, nil
}

func restoreIPTables(
type restorer struct {
cfg config.Config
ipv6 bool
dnsServers []string
executables *Executables
}

func newIPTablesRestorer(
ctx context.Context,
cfg config.Config,
dnsServers []string,
ipv6 bool,
) (string, error) {
executables, legacy, err := detectIptablesExecutables(ctx, cfg, ipv6)
dnsServers []string,
) (*restorer, error) {
executables, err := DetectIptablesExecutables(ctx, cfg, ipv6)
if err != nil {
return "", fmt.Errorf("unable to detect iptables restore binaries: %s", err)
return nil, fmt.Errorf("unable to detect iptables restore binaries: %s", err)
}

if executables.foundDockerOutputChain {
cfg.Redirect.DNS.UpstreamTargetChain = "DOCKER_OUTPUT"
}
return &restorer{
cfg: cfg,
ipv6: ipv6,
dnsServers: dnsServers,
executables: executables,
}, nil
}

rulesFile, err := createRulesFile(ipv6)
func (r *restorer) restore(ctx context.Context) (string, error) {
rulesFile, err := createRulesFile(r.ipv6)
if err != nil {
return "", err
}
defer rulesFile.Close()
defer os.Remove(rulesFile.Name())

err = configureIPv6Address(ipv6)
if err != nil {
if err := r.configureIPv6Address(); err != nil {
return "", err
}

rules, err := BuildIPTables(cfg, dnsServers, ipv6)
if err != nil {
return "", fmt.Errorf("unable to build iptable rules: %s", err)
}
for i := 0; i <= r.cfg.Retry.MaxRetries; i++ {
fmt.Fprintf(r.cfg.RuntimeStderr, "\n# [%d/%d] ", i+1, r.cfg.Retry.MaxRetries+1)

if err := saveIPTablesRestoreFile(cfg.RuntimeStdout, rulesFile, rules); err != nil {
return "", fmt.Errorf("unable to save iptables restore file: %s", err)
output, err := r.tryRestoreIPTables(ctx, r.executables, rulesFile)
if err == nil {
return output, nil
}

if r.executables.fallback != nil {
fmt.Fprintf(r.cfg.RuntimeStderr, ", trying fallback: ")

output, err := r.tryRestoreIPTables(ctx, r.executables.fallback, rulesFile)
if err == nil {
return output, nil
}
}

if i < r.cfg.Retry.MaxRetries {
fmt.Fprintf(r.cfg.RuntimeStderr, " will try again in %s", r.cfg.Retry.SleepBetweenReties)

time.Sleep(r.cfg.Retry.SleepBetweenReties)
}
}

return restoreIPTablesWithRetry(ctx, cfg, rulesFile, executables, legacy)
fmt.Fprintln(r.cfg.RuntimeStderr)

return "", errors.Errorf("%s failed", r.executables.Restore.Path)
}

func restoreIPTablesWithRetry(
func (r *restorer) tryRestoreIPTables(
ctx context.Context,
cfg config.Config,
executables *Executables,
rulesFile *os.File,
e *executables,
legacy bool,
) (string, error) {
params := buildRestoreParameters(cfg, rulesFile, legacy)
if executables.foundDockerOutputChain {
r.cfg.Redirect.DNS.UpstreamTargetChain = "DOCKER_OUTPUT"
}

for i := 0; i <= cfg.Retry.MaxRetries; i++ {
output, err := e.restore.exec(ctx, params...)
if err == nil {
return output.String(), nil
}
rules, err := BuildIPTables(r.cfg, r.dnsServers, r.ipv6, executables.Iptables.Path)
if err != nil {
return "", fmt.Errorf("unable to build iptable rules: %s", err)
}

_, _ = cfg.RuntimeStderr.Write([]byte(fmt.Sprintf(
"# [%d/%d] %s returned error: '%s'",
i+1,
cfg.Retry.MaxRetries+1,
strings.Join(append([]string{e.restore.path}, params...), " "),
err.Error(),
)))

if i < cfg.Retry.MaxRetries {
_, _ = cfg.RuntimeStderr.Write([]byte(fmt.Sprintf(
" will try again in %s",
cfg.Retry.SleepBetweenReties.String(),
)))

time.Sleep(cfg.Retry.SleepBetweenReties)
}
if err := r.saveIPTablesRestoreFile(rulesFile, rules); err != nil {
return "", fmt.Errorf("unable to save iptables restore file: %s", err)
}

params := buildRestoreParameters(r.cfg, rulesFile, executables.legacy)

fmt.Fprintf(r.cfg.RuntimeStderr, "%s %s", executables.Restore.Path, strings.Join(params, " "))

_, _ = cfg.RuntimeStderr.Write([]byte("\n"))
output, err := executables.Restore.exec(ctx, params...)
if err == nil {
return output.String(), nil
}

_, _ = cfg.RuntimeStderr.Write([]byte("\n"))
fmt.Fprintf(r.cfg.RuntimeStderr, " failed with error: '%s'", err)

return "", errors.Errorf("%s failed", e.restore.path)
return "", err
}

func RestoreIPTables(ctx context.Context, cfg config.Config) (string, error) {
Expand All @@ -214,21 +238,31 @@ func RestoreIPTables(ctx context.Context, cfg config.Config) (string, error) {
}
}

output, err := restoreIPTables(ctx, cfg, dnsIpv4, false)
ipv4Restorer, err := newIPTablesRestorer(ctx, cfg, false, dnsIpv4)
if err != nil {
return "", err
}

output, err := ipv4Restorer.restore(ctx)
if err != nil {
return "", fmt.Errorf("cannot restore ipv4 iptable rules: %s", err)
}

if cfg.IPv6 {
ipv6Output, err := restoreIPTables(ctx, cfg, dnsIpv6, true)
ipv6Restorer, err := newIPTablesRestorer(ctx, cfg, true, dnsIpv6)
if err != nil {
return "", err
}

ipv6Output, err := ipv6Restorer.restore(ctx)
if err != nil {
return "", fmt.Errorf("cannot restore ipv6 iptable rules: %s", err)
}

output += ipv6Output
}

_, _ = cfg.RuntimeStdout.Write([]byte("# iptables set to diverge the traffic " +
_, _ = cfg.RuntimeStdout.Write([]byte("\n# iptables set to diverge the traffic " +
"to Envoy.\n"))

return output, nil
Expand All @@ -238,8 +272,8 @@ func RestoreIPTables(ctx context.Context, cfg config.Config) (string, error) {
// for IPv6 but not IPv4, as IPv4 defaults to `netmask 255.0.0.0`, which allows binding to addresses
// in the 127.x.y.z range, while IPv6 defaults to `prefixlen 128` which allows binding only to ::1.
// Equivalent to `ip -6 addr add "::6/128" dev lo`
func configureIPv6Address(ipv6 bool) error {
if !ipv6 {
func (r *restorer) configureIPv6Address() error {
if !r.ipv6 {
return nil
}
link, err := netlink.LinkByName("lo")
Expand Down
Loading

0 comments on commit 35f57c2

Please sign in to comment.