Skip to content

Commit

Permalink
Fix #273 (#277), adding FIREWALL_OUTBOUND_SUBNETS
Browse files Browse the repository at this point in the history
  • Loading branch information
qdm12 authored Oct 29, 2020
1 parent f7bff24 commit db64dea
Show file tree
Hide file tree
Showing 16 changed files with 341 additions and 16 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ ENV VPNSP=pia \
FIREWALL=on \
FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \
FIREWALL_DEBUG=off \
# Tinyproxy
TINYPROXY=off \
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,16 +223,15 @@ 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_KEEP_NAMESERVER` | `off` | `on` or `off` | Keep the nameservers in /etc/resolv.conf untouched, but disabled DNS blocking features |
### Firewall
That one is important if you want to connect to the container from your LAN for example, using Shadowsocks or Tinyproxy.
### Firewall and routing
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `FIREWALL` | `on` | `on` or `off` | Turn on or off the container built-in firewall. You should use it for **debugging purposes** only. |
| `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_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
Expand Down
17 changes: 16 additions & 1 deletion cmd/gluetun/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,13 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
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 {
logger.Error(err)
Expand All @@ -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 {
logger.Warn(err)
err = ovpnConf.CreateTUN()
Expand Down
6 changes: 6 additions & 0 deletions internal/firewall/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ func (c *configurator) enable(ctx context.Context) (err error) {
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
// to reach Gluetun.
if err := c.acceptInputToSubnet(ctx, c.defaultInterface, c.localSubnet, remove); err != nil {
Expand Down
9 changes: 7 additions & 2 deletions internal/firewall/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ type Configurator interface {
SetEnabled(ctx context.Context, enabled bool) (err error)
SetVPNConnection(ctx context.Context, connection models.OpenVPNConnection) (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)
SetDebug()
// 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
Expand All @@ -34,11 +35,13 @@ type configurator struct { //nolint:maligned
defaultInterface string
defaultGateway net.IP
localSubnet net.IPNet
localIP net.IP
networkInfoMutex sync.Mutex

// State
enabled bool
vpnConnection models.OpenVPNConnection
outboundSubnets []net.IPNet
allowedInputPorts map[uint16]string // port to interface mapping
stateMutex sync.Mutex
}
Expand All @@ -58,10 +61,12 @@ func (c *configurator) SetDebug() {
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()
defer c.networkInfoMutex.Unlock()
c.defaultInterface = defaultInterface
c.defaultGateway = defaultGateway
c.localSubnet = localSubnet
c.localIP = localIP
}
13 changes: 13 additions & 0 deletions internal/firewall/iptables.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ func (c *configurator) acceptOutputTrafficToVPN(ctx context.Context,
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
Expand Down
56 changes: 56 additions & 0 deletions internal/firewall/outboundsubnets.go
Original file line number Diff line number Diff line change
@@ -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
}
53 changes: 53 additions & 0 deletions internal/firewall/subnets.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions internal/params/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Reader interface {
GetFirewall() (enabled bool, err error)
GetVPNInputPorts() (ports []uint16, err error)
GetInputPorts() (ports []uint16, err error)
GetOutboundSubnets() (outboundSubnets []net.IPNet, err error)
GetFirewallDebug() (debug bool, err error)

// VPN getters
Expand Down
31 changes: 31 additions & 0 deletions internal/params/routing.go
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 15 additions & 2 deletions internal/routing/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const (
)

func (r *routing) Setup() (err error) {
defaultIP, err := r.defaultIP()
defaultIP, err := r.DefaultIP()
if err != nil {
return fmt.Errorf("%s: %w", ErrSetup, err)
}
Expand All @@ -40,11 +40,19 @@ func (r *routing) Setup() (err error) {
if err := r.addRouteVia(defaultDestination, defaultGateway, defaultInterfaceName, table); err != nil {
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
}

func (r *routing) TearDown() error {
defaultIP, err := r.defaultIP()
defaultIP, err := r.DefaultIP()
if err != nil {
return fmt.Errorf("%s: %w", ErrTeardown, err)
}
Expand All @@ -60,5 +68,10 @@ func (r *routing) TearDown() error {
if err := r.deleteIPRule(defaultIP, table, priority); err != nil {
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
}
58 changes: 58 additions & 0 deletions internal/routing/outboundsubnets.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion internal/routing/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

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)
if err != nil {
return nil, fmt.Errorf("cannot get default IP address: %w", err)
Expand Down
Loading

0 comments on commit db64dea

Please sign in to comment.