From db64dea6642673cebd6378ebcf376f5b882b871f Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Thu, 29 Oct 2020 19:23:44 -0400 Subject: [PATCH] Fix #273 (#277), adding FIREWALL_OUTBOUND_SUBNETS --- Dockerfile | 1 + README.md | 5 +-- cmd/gluetun/main.go | 17 +++++++- internal/firewall/enable.go | 6 +++ internal/firewall/firewall.go | 9 ++++- internal/firewall/iptables.go | 13 +++++++ internal/firewall/outboundsubnets.go | 56 +++++++++++++++++++++++++++ internal/firewall/subnets.go | 53 +++++++++++++++++++++++++ internal/params/params.go | 1 + internal/params/routing.go | 31 +++++++++++++++ internal/routing/enable.go | 17 +++++++- internal/routing/outboundsubnets.go | 58 ++++++++++++++++++++++++++++ internal/routing/reader.go | 2 +- internal/routing/routing.go | 16 ++++++-- internal/routing/subnets.go | 53 +++++++++++++++++++++++++ internal/settings/firewall.go | 19 +++++++-- 16 files changed, 341 insertions(+), 16 deletions(-) create mode 100644 internal/firewall/outboundsubnets.go create mode 100644 internal/firewall/subnets.go create mode 100644 internal/params/routing.go create mode 100644 internal/routing/outboundsubnets.go create mode 100644 internal/routing/subnets.go diff --git a/Dockerfile b/Dockerfile index debb098e8..2b3448eca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/README.md b/README.md index 30116375c..b91683907 100644 --- a/README.md +++ b/README.md @@ -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_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 | | --- | --- | --- | --- | @@ -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_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 diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index f5f4d1aa7..a4bc89203 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -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) @@ -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() diff --git a/internal/firewall/enable.go b/internal/firewall/enable.go index f888a43d4..d89c92138 100644 --- a/internal/firewall/enable.go +++ b/internal/firewall/enable.go @@ -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 { diff --git a/internal/firewall/firewall.go b/internal/firewall/firewall.go index 838042bf2..d126c43a5 100644 --- a/internal/firewall/firewall.go +++ b/internal/firewall/firewall.go @@ -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 @@ -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 } @@ -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 } diff --git a/internal/firewall/iptables.go b/internal/firewall/iptables.go index 4f811c439..5e380b281 100644 --- a/internal/firewall/iptables.go +++ b/internal/firewall/iptables.go @@ -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 diff --git a/internal/firewall/outboundsubnets.go b/internal/firewall/outboundsubnets.go new file mode 100644 index 000000000..1bab38f85 --- /dev/null +++ b/internal/firewall/outboundsubnets.go @@ -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 +} diff --git a/internal/firewall/subnets.go b/internal/firewall/subnets.go new file mode 100644 index 000000000..7d74f5f14 --- /dev/null +++ b/internal/firewall/subnets.go @@ -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 +} diff --git a/internal/params/params.go b/internal/params/params.go index 9a96b32ef..369579f19 100644 --- a/internal/params/params.go +++ b/internal/params/params.go @@ -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 diff --git a/internal/params/routing.go b/internal/params/routing.go new file mode 100644 index 000000000..9b21e4754 --- /dev/null +++ b/internal/params/routing.go @@ -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 +} diff --git a/internal/routing/enable.go b/internal/routing/enable.go index 5028d9511..b116d52b2 100644 --- a/internal/routing/enable.go +++ b/internal/routing/enable.go @@ -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) } @@ -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) } @@ -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 } diff --git a/internal/routing/outboundsubnets.go b/internal/routing/outboundsubnets.go new file mode 100644 index 000000000..3ce30756e --- /dev/null +++ b/internal/routing/outboundsubnets.go @@ -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 +} diff --git a/internal/routing/reader.go b/internal/routing/reader.go index c5018156b..6eb6d26eb 100644 --- a/internal/routing/reader.go +++ b/internal/routing/reader.go @@ -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) diff --git a/internal/routing/routing.go b/internal/routing/routing.go index 126722386..4a2c34d0d 100644 --- a/internal/routing/routing.go +++ b/internal/routing/routing.go @@ -2,25 +2,35 @@ package routing import ( "net" + "sync" "github.com/qdm12/golibs/logging" ) type Routing interface { + // Mutations Setup() (err error) TearDown() error + SetOutboundRoutes(outboundSubnets []net.IPNet) error + + // Read only DefaultRoute() (defaultInterface string, defaultGateway net.IP, err error) LocalSubnet() (defaultSubnet net.IPNet, err error) + DefaultIP() (defaultIP net.IP, err error) VPNDestinationIP() (ip net.IP, err error) VPNLocalGatewayIP() (ip net.IP, err error) + + // Internal state SetVerbose(verbose bool) SetDebug() } type routing struct { - logger logging.Logger - verbose bool - debug bool + logger logging.Logger + verbose bool + debug bool + outboundSubnets []net.IPNet + stateMutex sync.RWMutex } // NewConfigurator creates a new Configurator instance. diff --git a/internal/routing/subnets.go b/internal/routing/subnets.go new file mode 100644 index 000000000..ac7d4aa84 --- /dev/null +++ b/internal/routing/subnets.go @@ -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 +} diff --git a/internal/settings/firewall.go b/internal/settings/firewall.go index 3154d74c4..1cfb77c6a 100644 --- a/internal/settings/firewall.go +++ b/internal/settings/firewall.go @@ -2,6 +2,7 @@ package settings import ( "fmt" + "net" "strings" "github.com/qdm12/gluetun/internal/params" @@ -9,10 +10,11 @@ import ( // Firewall contains settings to customize the firewall operation. type Firewall struct { - VPNInputPorts []uint16 - InputPorts []uint16 - Enabled bool - Debug bool + VPNInputPorts []uint16 + InputPorts []uint16 + OutboundSubnets []net.IPNet + Enabled bool + Debug bool } func (f *Firewall) String() string { @@ -27,11 +29,16 @@ func (f *Firewall) String() string { for i, port := range f.InputPorts { 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{ "Firewall settings:", "VPN input ports: " + strings.Join(vpnInputPorts, ", "), "Input ports: " + strings.Join(inputPorts, ", "), + "Outbound subnets: " + strings.Join(outboundSubnets, ", "), } if f.Debug { settingsList = append(settingsList, "Debug: on") @@ -49,6 +56,10 @@ func GetFirewallSettings(paramsReader params.Reader) (settings Firewall, err err if err != nil { return settings, err } + settings.OutboundSubnets, err = paramsReader.GetOutboundSubnets() + if err != nil { + return settings, err + } settings.Enabled, err = paramsReader.GetFirewall() if err != nil { return settings, err