Fix #273 (#277), adding FIREWALL_OUTBOUND_SUBNETS

This commit is contained in:
Quentin McGaw
2020-10-29 19:23:44 -04:00
committed by GitHub
parent f7bff247aa
commit db64dea664
16 changed files with 341 additions and 16 deletions

View File

@@ -91,6 +91,7 @@ ENV VPNSP=pia \
FIREWALL=on \ FIREWALL=on \
FIREWALL_VPN_INPUT_PORTS= \ FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \ FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \
FIREWALL_DEBUG=off \ FIREWALL_DEBUG=off \
# Tinyproxy # Tinyproxy
TINYPROXY=off \ TINYPROXY=off \

View File

@@ -223,9 +223,7 @@ None of the following values are required.
| `DNS_PLAINTEXT_ADDRESS` | `1.1.1.1` | Any IP address | IP address to use as DNS resolver if `DOT` is `off` | | `DNS_PLAINTEXT_ADDRESS` | `1.1.1.1` | Any IP address | IP address to use as DNS resolver if `DOT` is `off` |
| `DNS_KEEP_NAMESERVER` | `off` | `on` or `off` | Keep the nameservers in /etc/resolv.conf untouched, but disabled DNS blocking features | | `DNS_KEEP_NAMESERVER` | `off` | `on` or `off` | Keep the nameservers in /etc/resolv.conf untouched, but disabled DNS blocking features |
### Firewall ### Firewall and routing
That one is important if you want to connect to the container from your LAN for example, using Shadowsocks or Tinyproxy.
| Variable | Default | Choices | Description | | Variable | Default | Choices | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
@@ -233,6 +231,7 @@ That one is important if you want to connect to the container from your LAN for
| `FIREWALL_VPN_INPUT_PORTS` | | i.e. `1000,8080` | Comma separated list of ports to allow from the VPN server side (useful for **vyprvpn** port forwarding) | | `FIREWALL_VPN_INPUT_PORTS` | | i.e. `1000,8080` | Comma separated list of ports to allow from the VPN server side (useful for **vyprvpn** port forwarding) |
| `FIREWALL_INPUT_PORTS` | | i.e. `1000,8000` | Comma separated list of ports to allow through the default interface. This seems needed for Kubernetes sidecars. | | `FIREWALL_INPUT_PORTS` | | i.e. `1000,8000` | Comma separated list of ports to allow through the default interface. This seems needed for Kubernetes sidecars. |
| `FIREWALL_DEBUG` | `off` | `on` or `off` | Prints every firewall related command. You should use it for **debugging purposes** only. | | `FIREWALL_DEBUG` | `off` | `on` or `off` | Prints every firewall related command. You should use it for **debugging purposes** only. |
| `FIREWALL_OUTBOUND_SUBNETS` | | i.e. `192.168.1.0/24,192.168.10.121,10.0.0.5/28` | Comma separated subnets that Gluetun and the containers sharing its network stack are allowed to access. This involves firewall and routing modifications. |
### Shadowsocks ### Shadowsocks

View File

@@ -148,7 +148,13 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
return 1 return 1
} }
firewallConf.SetNetworkInformation(defaultInterface, defaultGateway, localSubnet) defaultIP, err := routingConf.DefaultIP()
if err != nil {
logger.Error(err)
return 1
}
firewallConf.SetNetworkInformation(defaultInterface, defaultGateway, localSubnet, defaultIP)
if err := routingConf.Setup(); err != nil { if err := routingConf.Setup(); err != nil {
logger.Error(err) logger.Error(err)
@@ -160,6 +166,15 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
} }
}() }()
if err := firewallConf.SetOutboundSubnets(ctx, allSettings.Firewall.OutboundSubnets); err != nil {
logger.Error(err)
return 1
}
if err := routingConf.SetOutboundRoutes(allSettings.Firewall.OutboundSubnets); err != nil {
logger.Error(err)
return 1
}
if err := ovpnConf.CheckTUN(); err != nil { if err := ovpnConf.CheckTUN(); err != nil {
logger.Warn(err) logger.Warn(err)
err = ovpnConf.CreateTUN() err = ovpnConf.CreateTUN()

View File

@@ -94,6 +94,12 @@ func (c *configurator) enable(ctx context.Context) (err error) {
return fmt.Errorf("cannot enable firewall: %w", err) return fmt.Errorf("cannot enable firewall: %w", err)
} }
for _, subnet := range c.outboundSubnets {
if err := c.acceptOutputFromIPToSubnet(ctx, c.defaultInterface, c.localIP, subnet, remove); err != nil {
return fmt.Errorf("cannot enable firewall: %w", err)
}
}
// Allows packets from any IP address to go through eth0 / local network // Allows packets from any IP address to go through eth0 / local network
// to reach Gluetun. // to reach Gluetun.
if err := c.acceptInputToSubnet(ctx, c.defaultInterface, c.localSubnet, remove); err != nil { if err := c.acceptInputToSubnet(ctx, c.defaultInterface, c.localSubnet, remove); err != nil {

View File

@@ -18,10 +18,11 @@ type Configurator interface {
SetEnabled(ctx context.Context, enabled bool) (err error) SetEnabled(ctx context.Context, enabled bool) (err error)
SetVPNConnection(ctx context.Context, connection models.OpenVPNConnection) (err error) SetVPNConnection(ctx context.Context, connection models.OpenVPNConnection) (err error)
SetAllowedPort(ctx context.Context, port uint16, intf string) (err error) SetAllowedPort(ctx context.Context, port uint16, intf string) (err error)
SetOutboundSubnets(ctx context.Context, subnets []net.IPNet) (err error)
RemoveAllowedPort(ctx context.Context, port uint16) (err error) RemoveAllowedPort(ctx context.Context, port uint16) (err error)
SetDebug() SetDebug()
// SetNetworkInformation is meant to be called only once // SetNetworkInformation is meant to be called only once
SetNetworkInformation(defaultInterface string, defaultGateway net.IP, localSubnet net.IPNet) SetNetworkInformation(defaultInterface string, defaultGateway net.IP, localSubnet net.IPNet, localIP net.IP)
} }
type configurator struct { //nolint:maligned type configurator struct { //nolint:maligned
@@ -34,11 +35,13 @@ type configurator struct { //nolint:maligned
defaultInterface string defaultInterface string
defaultGateway net.IP defaultGateway net.IP
localSubnet net.IPNet localSubnet net.IPNet
localIP net.IP
networkInfoMutex sync.Mutex networkInfoMutex sync.Mutex
// State // State
enabled bool enabled bool
vpnConnection models.OpenVPNConnection vpnConnection models.OpenVPNConnection
outboundSubnets []net.IPNet
allowedInputPorts map[uint16]string // port to interface mapping allowedInputPorts map[uint16]string // port to interface mapping
stateMutex sync.Mutex stateMutex sync.Mutex
} }
@@ -58,10 +61,12 @@ func (c *configurator) SetDebug() {
c.debug = true c.debug = true
} }
func (c *configurator) SetNetworkInformation(defaultInterface string, defaultGateway net.IP, localSubnet net.IPNet) { func (c *configurator) SetNetworkInformation(
defaultInterface string, defaultGateway net.IP, localSubnet net.IPNet, localIP net.IP) {
c.networkInfoMutex.Lock() c.networkInfoMutex.Lock()
defer c.networkInfoMutex.Unlock() defer c.networkInfoMutex.Unlock()
c.defaultInterface = defaultInterface c.defaultInterface = defaultInterface
c.defaultGateway = defaultGateway c.defaultGateway = defaultGateway
c.localSubnet = localSubnet c.localSubnet = localSubnet
c.localIP = localIP
} }

View File

@@ -124,6 +124,19 @@ func (c *configurator) acceptOutputTrafficToVPN(ctx context.Context,
appendOrDelete(remove), connection.IP, defaultInterface, connection.Protocol, connection.Protocol, connection.Port)) 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. // Used for port forwarding, with intf set to tun.
func (c *configurator) acceptInputToPort(ctx context.Context, intf string, port uint16, remove bool) error { func (c *configurator) acceptInputToPort(ctx context.Context, intf string, port uint16, remove bool) error {
interfaceFlag := "-i " + intf interfaceFlag := "-i " + intf

View File

@@ -0,0 +1,56 @@
package firewall
import (
"context"
"fmt"
"net"
)
func (c *configurator) SetOutboundSubnets(ctx context.Context, subnets []net.IPNet) (err error) {
c.stateMutex.Lock()
defer c.stateMutex.Unlock()
if !c.enabled {
c.logger.Info("firewall disabled, only updating allowed subnets internal list")
c.outboundSubnets = make([]net.IPNet, len(subnets))
copy(c.outboundSubnets, subnets)
return nil
}
c.logger.Info("setting allowed subnets through firewall...")
subnetsToAdd := findSubnetsToAdd(c.outboundSubnets, subnets)
subnetsToRemove := findSubnetsToRemove(c.outboundSubnets, subnets)
if len(subnetsToAdd) == 0 && len(subnetsToRemove) == 0 {
return nil
}
c.removeOutboundSubnets(ctx, subnetsToRemove)
if err := c.addOutboundSubnets(ctx, subnetsToAdd); err != nil {
return fmt.Errorf("cannot set allowed subnets through firewall: %w", err)
}
return nil
}
func (c *configurator) removeOutboundSubnets(ctx context.Context, subnets []net.IPNet) {
const remove = true
for _, subnet := range subnets {
if err := c.acceptOutputFromIPToSubnet(ctx, c.defaultInterface, c.localIP, subnet, remove); err != nil {
c.logger.Error("cannot remove outdated outbound subnet through firewall: %s", err)
continue
}
c.outboundSubnets = removeSubnetFromSubnets(c.outboundSubnets, subnet)
}
}
func (c *configurator) addOutboundSubnets(ctx context.Context, subnets []net.IPNet) error {
const remove = false
for _, subnet := range subnets {
if err := c.acceptOutputFromIPToSubnet(ctx, c.defaultInterface, c.localIP, subnet, remove); err != nil {
return fmt.Errorf("cannot add allowed subnet through firewall: %w", err)
}
c.outboundSubnets = append(c.outboundSubnets, subnet)
}
return nil
}

View File

@@ -0,0 +1,53 @@
package firewall
import (
"net"
)
func findSubnetsToAdd(oldSubnets, newSubnets []net.IPNet) (subnetsToAdd []net.IPNet) {
for _, newSubnet := range newSubnets {
found := false
for _, oldSubnet := range oldSubnets {
if subnetsAreEqual(oldSubnet, newSubnet) {
found = true
break
}
}
if !found {
subnetsToAdd = append(subnetsToAdd, newSubnet)
}
}
return subnetsToAdd
}
func findSubnetsToRemove(oldSubnets, newSubnets []net.IPNet) (subnetsToRemove []net.IPNet) {
for _, oldSubnet := range oldSubnets {
found := false
for _, newSubnet := range newSubnets {
if subnetsAreEqual(oldSubnet, newSubnet) {
found = true
break
}
}
if !found {
subnetsToRemove = append(subnetsToRemove, oldSubnet)
}
}
return subnetsToRemove
}
func subnetsAreEqual(a, b net.IPNet) bool {
return a.IP.Equal(b.IP) && a.Mask.String() == b.Mask.String()
}
func removeSubnetFromSubnets(subnets []net.IPNet, subnet net.IPNet) []net.IPNet {
L := len(subnets)
for i := range subnets {
if subnetsAreEqual(subnet, subnets[i]) {
subnets[i] = subnets[L-1]
subnets = subnets[:L-1]
break
}
}
return subnets
}

View File

@@ -43,6 +43,7 @@ type Reader interface {
GetFirewall() (enabled bool, err error) GetFirewall() (enabled bool, err error)
GetVPNInputPorts() (ports []uint16, err error) GetVPNInputPorts() (ports []uint16, err error)
GetInputPorts() (ports []uint16, err error) GetInputPorts() (ports []uint16, err error)
GetOutboundSubnets() (outboundSubnets []net.IPNet, err error)
GetFirewallDebug() (debug bool, err error) GetFirewallDebug() (debug bool, err error)
// VPN getters // VPN getters

View File

@@ -0,0 +1,31 @@
package params
import (
"fmt"
"net"
"strings"
)
// GetOutboundSubnets obtains the CIDR subnets from the comma separated list of the
// environment variable FIREWALL_OUTBOUND_SUBNETS.
func (r *reader) GetOutboundSubnets() (outboundSubnets []net.IPNet, err error) {
const key = "FIREWALL_OUTBOUND_SUBNETS"
s, err := r.envParams.GetEnv(key)
if err != nil {
return nil, err
} else if s == "" {
return nil, nil
}
subnets := strings.Split(s, ",")
for _, subnet := range subnets {
_, cidr, err := net.ParseCIDR(subnet)
if err != nil {
return nil, fmt.Errorf("cannot parse outbound subnet %q from environment variable with key %s: %w", subnet, key, err)
} else if cidr == nil {
return nil, fmt.Errorf("cannot parse outbound subnet %q from environment variable with key %s: subnet is nil",
subnet, key)
}
outboundSubnets = append(outboundSubnets, *cidr)
}
return outboundSubnets, nil
}

View File

@@ -16,7 +16,7 @@ const (
) )
func (r *routing) Setup() (err error) { func (r *routing) Setup() (err error) {
defaultIP, err := r.defaultIP() defaultIP, err := r.DefaultIP()
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", ErrSetup, err) return fmt.Errorf("%s: %w", ErrSetup, err)
} }
@@ -40,11 +40,19 @@ func (r *routing) Setup() (err error) {
if err := r.addRouteVia(defaultDestination, defaultGateway, defaultInterfaceName, table); err != nil { if err := r.addRouteVia(defaultDestination, defaultGateway, defaultInterfaceName, table); err != nil {
return fmt.Errorf("%s: %w", ErrSetup, err) return fmt.Errorf("%s: %w", ErrSetup, err)
} }
r.stateMutex.RLock()
outboundSubnets := r.outboundSubnets
r.stateMutex.RUnlock()
if err := r.setOutboundRoutes(outboundSubnets, defaultInterfaceName, defaultGateway); err != nil {
return fmt.Errorf("%s: %w", ErrSetup, err)
}
return nil return nil
} }
func (r *routing) TearDown() error { func (r *routing) TearDown() error {
defaultIP, err := r.defaultIP() defaultIP, err := r.DefaultIP()
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", ErrTeardown, err) return fmt.Errorf("%s: %w", ErrTeardown, err)
} }
@@ -60,5 +68,10 @@ func (r *routing) TearDown() error {
if err := r.deleteIPRule(defaultIP, table, priority); err != nil { if err := r.deleteIPRule(defaultIP, table, priority); err != nil {
return fmt.Errorf("%s: %w", ErrTeardown, err) return fmt.Errorf("%s: %w", ErrTeardown, err)
} }
if err := r.setOutboundRoutes(nil, defaultInterfaceName, defaultGateway); err != nil {
return fmt.Errorf("%s: %w", ErrSetup, err)
}
return nil return nil
} }

View File

@@ -0,0 +1,58 @@
package routing
import (
"fmt"
"net"
)
func (r *routing) SetOutboundRoutes(outboundSubnets []net.IPNet) error {
defaultInterface, defaultGateway, err := r.DefaultRoute()
if err != nil {
return fmt.Errorf("cannot set oubtound subnets in routing: %w", err)
}
return r.setOutboundRoutes(outboundSubnets, defaultInterface, defaultGateway)
}
func (r *routing) setOutboundRoutes(outboundSubnets []net.IPNet,
defaultInterfaceName string, defaultGateway net.IP) error {
r.stateMutex.Lock()
defer r.stateMutex.Unlock()
subnetsToRemove := findSubnetsToRemove(r.outboundSubnets, outboundSubnets)
subnetsToAdd := findSubnetsToAdd(r.outboundSubnets, outboundSubnets)
if len(subnetsToAdd) == 0 && len(subnetsToRemove) == 0 {
return nil
}
r.removeOutboundSubnets(subnetsToRemove, defaultInterfaceName, defaultGateway)
if err := r.addOutboundSubnets(subnetsToAdd, defaultInterfaceName, defaultGateway); err != nil {
return fmt.Errorf("cannot set outbound subnets in routing: %w", err)
}
return nil
}
func (r *routing) removeOutboundSubnets(subnets []net.IPNet,
defaultInterfaceName string, defaultGateway net.IP) {
for _, subnet := range subnets {
const table = 0
if err := r.deleteRouteVia(subnet, defaultGateway, defaultInterfaceName, table); err != nil {
r.logger.Error("cannot remove outdated outbound subnet from routing: %s", err)
continue
}
r.outboundSubnets = removeSubnetFromSubnets(r.outboundSubnets, subnet)
}
}
func (r *routing) addOutboundSubnets(subnets []net.IPNet,
defaultInterfaceName string, defaultGateway net.IP) error {
for _, subnet := range subnets {
const table = 0
if err := r.addRouteVia(subnet, defaultGateway, defaultInterfaceName, table); err != nil {
return fmt.Errorf("cannot add outbound subnet %s to routing: %w", subnet, err)
}
r.outboundSubnets = append(r.outboundSubnets, subnet)
}
return nil
}

View File

@@ -33,7 +33,7 @@ func (r *routing) DefaultRoute() (defaultInterface string, defaultGateway net.IP
return "", nil, fmt.Errorf("cannot find default route in %d routes", len(routes)) return "", nil, fmt.Errorf("cannot find default route in %d routes", len(routes))
} }
func (r *routing) defaultIP() (ip net.IP, err error) { func (r *routing) DefaultIP() (ip net.IP, err error) {
routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL) routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get default IP address: %w", err) return nil, fmt.Errorf("cannot get default IP address: %w", err)

View File

@@ -2,17 +2,25 @@ package routing
import ( import (
"net" "net"
"sync"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
) )
type Routing interface { type Routing interface {
// Mutations
Setup() (err error) Setup() (err error)
TearDown() error TearDown() error
SetOutboundRoutes(outboundSubnets []net.IPNet) error
// Read only
DefaultRoute() (defaultInterface string, defaultGateway net.IP, err error) DefaultRoute() (defaultInterface string, defaultGateway net.IP, err error)
LocalSubnet() (defaultSubnet net.IPNet, err error) LocalSubnet() (defaultSubnet net.IPNet, err error)
DefaultIP() (defaultIP net.IP, err error)
VPNDestinationIP() (ip net.IP, err error) VPNDestinationIP() (ip net.IP, err error)
VPNLocalGatewayIP() (ip net.IP, err error) VPNLocalGatewayIP() (ip net.IP, err error)
// Internal state
SetVerbose(verbose bool) SetVerbose(verbose bool)
SetDebug() SetDebug()
} }
@@ -21,6 +29,8 @@ type routing struct {
logger logging.Logger logger logging.Logger
verbose bool verbose bool
debug bool debug bool
outboundSubnets []net.IPNet
stateMutex sync.RWMutex
} }
// NewConfigurator creates a new Configurator instance. // NewConfigurator creates a new Configurator instance.

View File

@@ -0,0 +1,53 @@
package routing
import (
"net"
)
func findSubnetsToAdd(oldSubnets, newSubnets []net.IPNet) (subnetsToAdd []net.IPNet) {
for _, newSubnet := range newSubnets {
found := false
for _, oldSubnet := range oldSubnets {
if subnetsAreEqual(oldSubnet, newSubnet) {
found = true
break
}
}
if !found {
subnetsToAdd = append(subnetsToAdd, newSubnet)
}
}
return subnetsToAdd
}
func findSubnetsToRemove(oldSubnets, newSubnets []net.IPNet) (subnetsToRemove []net.IPNet) {
for _, oldSubnet := range oldSubnets {
found := false
for _, newSubnet := range newSubnets {
if subnetsAreEqual(oldSubnet, newSubnet) {
found = true
break
}
}
if !found {
subnetsToRemove = append(subnetsToRemove, oldSubnet)
}
}
return subnetsToRemove
}
func subnetsAreEqual(a, b net.IPNet) bool {
return a.IP.Equal(b.IP) && a.Mask.String() == b.Mask.String()
}
func removeSubnetFromSubnets(subnets []net.IPNet, subnet net.IPNet) []net.IPNet {
L := len(subnets)
for i := range subnets {
if subnetsAreEqual(subnet, subnets[i]) {
subnets[i] = subnets[L-1]
subnets = subnets[:L-1]
break
}
}
return subnets
}

View File

@@ -2,6 +2,7 @@ package settings
import ( import (
"fmt" "fmt"
"net"
"strings" "strings"
"github.com/qdm12/gluetun/internal/params" "github.com/qdm12/gluetun/internal/params"
@@ -11,6 +12,7 @@ import (
type Firewall struct { type Firewall struct {
VPNInputPorts []uint16 VPNInputPorts []uint16
InputPorts []uint16 InputPorts []uint16
OutboundSubnets []net.IPNet
Enabled bool Enabled bool
Debug bool Debug bool
} }
@@ -27,11 +29,16 @@ func (f *Firewall) String() string {
for i, port := range f.InputPorts { for i, port := range f.InputPorts {
inputPorts[i] = fmt.Sprintf("%d", port) inputPorts[i] = fmt.Sprintf("%d", port)
} }
outboundSubnets := make([]string, len(f.OutboundSubnets))
for i := range f.OutboundSubnets {
outboundSubnets[i] = f.OutboundSubnets[i].String()
}
settingsList := []string{ settingsList := []string{
"Firewall settings:", "Firewall settings:",
"VPN input ports: " + strings.Join(vpnInputPorts, ", "), "VPN input ports: " + strings.Join(vpnInputPorts, ", "),
"Input ports: " + strings.Join(inputPorts, ", "), "Input ports: " + strings.Join(inputPorts, ", "),
"Outbound subnets: " + strings.Join(outboundSubnets, ", "),
} }
if f.Debug { if f.Debug {
settingsList = append(settingsList, "Debug: on") settingsList = append(settingsList, "Debug: on")
@@ -49,6 +56,10 @@ func GetFirewallSettings(paramsReader params.Reader) (settings Firewall, err err
if err != nil { if err != nil {
return settings, err return settings, err
} }
settings.OutboundSubnets, err = paramsReader.GetOutboundSubnets()
if err != nil {
return settings, err
}
settings.Enabled, err = paramsReader.GetFirewall() settings.Enabled, err = paramsReader.GetFirewall()
if err != nil { if err != nil {
return settings, err return settings, err