Files
gluetun/internal/firewall/iptables.go
2021-01-31 18:45:58 +00:00

195 lines
5.9 KiB
Go

package firewall
import (
"context"
"fmt"
"io/ioutil"
"net"
"os"
"strings"
"github.com/qdm12/gluetun/internal/models"
)
func appendOrDelete(remove bool) string {
if remove {
return "--delete"
}
return "--append"
}
// flipRule changes an append rule in a delete rule or a delete rule into an
// append rule.
func flipRule(rule string) string {
switch {
case strings.HasPrefix(rule, "-A"):
return strings.Replace(rule, "-A", "-D", 1)
case strings.HasPrefix(rule, "--append"):
return strings.Replace(rule, "--append", "-D", 1)
case strings.HasPrefix(rule, "-D"):
return strings.Replace(rule, "-D", "-A", 1)
case strings.HasPrefix(rule, "--delete"):
return strings.Replace(rule, "--delete", "-A", 1)
}
return rule
}
// Version obtains the version of the installed iptables.
func (c *configurator) Version(ctx context.Context) (string, error) {
output, err := c.commander.Run(ctx, "iptables", "--version")
if err != nil {
return "", err
}
words := strings.Fields(output)
const minWords = 2
if len(words) < minWords {
return "", fmt.Errorf("iptables --version: output is too short: %q", output)
}
return words[1], nil
}
func (c *configurator) runIptablesInstructions(ctx context.Context, instructions []string) error {
for _, instruction := range instructions {
if err := c.runIptablesInstruction(ctx, instruction); err != nil {
return err
}
}
return nil
}
func (c *configurator) runIptablesInstruction(ctx context.Context, instruction string) error {
c.iptablesMutex.Lock() // only one iptables command at once
defer c.iptablesMutex.Unlock()
if c.debug {
fmt.Printf("iptables %s\n", instruction)
}
flags := strings.Fields(instruction)
if output, err := c.commander.Run(ctx, "iptables", flags...); err != nil {
return fmt.Errorf("failed executing \"iptables %s\": %s: %w", instruction, output, err)
}
return nil
}
func (c *configurator) clearAllRules(ctx context.Context) error {
return c.runIptablesInstructions(ctx, []string{
"--flush", // flush all chains
"--delete-chain", // delete all chains
})
}
func (c *configurator) setAllPolicies(ctx context.Context, policy string) error {
switch policy {
case "ACCEPT", "DROP":
default:
return fmt.Errorf("policy %q not recognized", policy)
}
return c.runIptablesInstructions(ctx, []string{
fmt.Sprintf("--policy INPUT %s", policy),
fmt.Sprintf("--policy OUTPUT %s", policy),
fmt.Sprintf("--policy FORWARD %s", policy),
})
}
func (c *configurator) acceptInputThroughInterface(ctx context.Context, intf string, remove bool) error {
return c.runIptablesInstruction(ctx, fmt.Sprintf(
"%s INPUT -i %s -j ACCEPT", appendOrDelete(remove), intf,
))
}
func (c *configurator) acceptInputToSubnet(ctx context.Context, intf string, destination net.IPNet, remove bool) error {
interfaceFlag := "-i " + intf
if intf == "*" { // all interfaces
interfaceFlag = ""
}
return c.runIptablesInstruction(ctx, fmt.Sprintf(
"%s INPUT %s -d %s -j ACCEPT", appendOrDelete(remove), interfaceFlag, destination.String(),
))
}
func (c *configurator) acceptOutputThroughInterface(ctx context.Context, intf string, remove bool) error {
return c.runIptablesInstruction(ctx, fmt.Sprintf(
"%s OUTPUT -o %s -j ACCEPT", appendOrDelete(remove), intf,
))
}
func (c *configurator) acceptEstablishedRelatedTraffic(ctx context.Context, remove bool) error {
return c.runIptablesInstructions(ctx, []string{
fmt.Sprintf("%s OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT", appendOrDelete(remove)),
fmt.Sprintf("%s INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT", appendOrDelete(remove)),
})
}
func (c *configurator) acceptOutputTrafficToVPN(ctx context.Context,
defaultInterface string, connection models.OpenVPNConnection, remove bool) error {
return c.runIptablesInstruction(ctx,
fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
appendOrDelete(remove), connection.IP, defaultInterface, connection.Protocol, connection.Protocol, connection.Port))
}
// Thanks to @npawelek.
func (c *configurator) acceptOutputFromIPToSubnet(ctx context.Context,
intf string, sourceIP net.IP, destinationSubnet net.IPNet, remove bool) error {
interfaceFlag := "-o " + intf
if intf == "*" { // all interfaces
interfaceFlag = ""
}
return c.runIptablesInstruction(ctx, fmt.Sprintf(
"%s OUTPUT %s -s %s -d %s -j ACCEPT",
appendOrDelete(remove), interfaceFlag, sourceIP.String(), destinationSubnet.String(),
))
}
// Used for port forwarding, with intf set to tun.
func (c *configurator) acceptInputToPort(ctx context.Context, intf string, port uint16, remove bool) error {
interfaceFlag := "-i " + intf
if intf == "*" { // all interfaces
interfaceFlag = ""
}
return c.runIptablesInstructions(ctx, []string{
fmt.Sprintf("%s INPUT %s -p tcp --dport %d -j ACCEPT", appendOrDelete(remove), interfaceFlag, port),
fmt.Sprintf("%s INPUT %s -p udp --dport %d -j ACCEPT", appendOrDelete(remove), interfaceFlag, port),
})
}
func (c *configurator) runUserPostRules(ctx context.Context, filepath string, remove bool) error {
file, err := c.openFile(filepath, os.O_RDONLY, 0)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return err
}
b, err := ioutil.ReadAll(file)
if err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
lines := strings.Split(string(b), "\n")
successfulRules := []string{}
defer func() {
// transaction-like rollback
if err == nil || ctx.Err() != nil {
return
}
for _, rule := range successfulRules {
_ = c.runIptablesInstruction(ctx, flipRule(rule))
}
}()
for _, line := range lines {
if !strings.HasPrefix(line, "iptables ") {
continue
}
rule := strings.TrimPrefix(line, "iptables ")
if remove {
rule = flipRule(rule)
}
if err = c.runIptablesInstruction(ctx, rule); err != nil {
return fmt.Errorf("cannot run custom rule: %w", err)
}
successfulRules = append(successfulRules, rule)
}
return nil
}