Routing improvements (#268)
- Fixes #82 - Remove `EXTRA_SUBNETS` - Remove no longer needed iptables rules - Reduce routing interface arity - Routing setup is done in main.go instead of in the firewall - Routing setup gets reverted at shutdown
This commit is contained in:
@@ -89,7 +89,6 @@ ENV VPNSP=pia \
|
|||||||
DNS_KEEP_NAMESERVER=off \
|
DNS_KEEP_NAMESERVER=off \
|
||||||
# Firewall
|
# Firewall
|
||||||
FIREWALL=on \
|
FIREWALL=on \
|
||||||
EXTRA_SUBNETS= \
|
|
||||||
FIREWALL_VPN_INPUT_PORTS= \
|
FIREWALL_VPN_INPUT_PORTS= \
|
||||||
FIREWALL_INPUT_PORTS= \
|
FIREWALL_INPUT_PORTS= \
|
||||||
FIREWALL_DEBUG=off \
|
FIREWALL_DEBUG=off \
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ iptables, DNS over TLS, ShadowSocks and Tinyproxy*
|
|||||||
Note that you can:
|
Note that you can:
|
||||||
|
|
||||||
- Change the many [environment variables](#environment-variables) available
|
- Change the many [environment variables](#environment-variables) available
|
||||||
- Use `-p 8888:8888/tcp` to access the HTTP web proxy (and put your LAN in `EXTRA_SUBNETS` environment variable, in example `192.168.1.0/24`)
|
- Use `-p 8888:8888/tcp` to access the HTTP web proxy
|
||||||
- Use `-p 8388:8388/tcp -p 8388:8388/udp` to access the Shadowsocks proxy (and put your LAN in `EXTRA_SUBNETS` environment variable, in example `192.168.1.0/24`)
|
- Use `-p 8388:8388/tcp -p 8388:8388/udp` to access the Shadowsocks proxy
|
||||||
- Use `-p 8000:8000/tcp` to access the [HTTP control server](#HTTP-control-server) built-in
|
- Use `-p 8000:8000/tcp` to access the [HTTP control server](#HTTP-control-server) built-in
|
||||||
|
|
||||||
**If you encounter an issue with the tun device not being available, see [the FAQ](https://github.com/qdm12/gluetun/blob/master/doc/faq.md#how-to-fix-openvpn-failing-to-start)**
|
**If you encounter an issue with the tun device not being available, see [the FAQ](https://github.com/qdm12/gluetun/blob/master/doc/faq.md#how-to-fix-openvpn-failing-to-start)**
|
||||||
@@ -230,7 +230,6 @@ That one is important if you want to connect to the container from your LAN for
|
|||||||
| Variable | Default | Choices | Description |
|
| 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` | `on` | `on` or `off` | Turn on or off the container built-in firewall. You should use it for **debugging purposes** only. |
|
||||||
| `EXTRA_SUBNETS` | | i.e. `192.168.1.0/24,192.168.10.121,10.0.0.5/28` | Comma separated subnets allowed in the container firewall |
|
|
||||||
| `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. |
|
||||||
@@ -304,7 +303,6 @@ There are various ways to achieve this, depending on your use case.
|
|||||||
1. Setup a HTTP proxy client, such as [SwitchyOmega for Chrome](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif?hl=en)
|
1. Setup a HTTP proxy client, such as [SwitchyOmega for Chrome](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif?hl=en)
|
||||||
1. Ensure the Gluetun container is launched with:
|
1. Ensure the Gluetun container is launched with:
|
||||||
- port `8888` published `-p 8888:8888/tcp`
|
- port `8888` published `-p 8888:8888/tcp`
|
||||||
- your LAN subnet, i.e. `192.168.1.0/24`, set as `-e EXTRA_SUBNETS=192.168.1.0/24`
|
|
||||||
1. With your HTTP proxy client, connect to the Docker host (i.e. `192.168.1.10`) on port `8888`. You need to enter your credentials if you set them with `TINYPROXY_USER` and `TINYPROXY_PASSWORD`.
|
1. With your HTTP proxy client, connect to the Docker host (i.e. `192.168.1.10`) on port `8888`. You need to enter your credentials if you set them with `TINYPROXY_USER` and `TINYPROXY_PASSWORD`.
|
||||||
1. If you set `TINYPROXY_LOG` to `Info`, more information will be logged in the Docker logs
|
1. If you set `TINYPROXY_LOG` to `Info`, more information will be logged in the Docker logs
|
||||||
|
|
||||||
@@ -319,7 +317,6 @@ There are various ways to achieve this, depending on your use case.
|
|||||||
- Android: Shadowsocks by Max Lv
|
- Android: Shadowsocks by Max Lv
|
||||||
1. Ensure the Gluetun container is launched with:
|
1. Ensure the Gluetun container is launched with:
|
||||||
- port `8388` published `-p 8388:8388/tcp -p 8388:8388/udp`
|
- port `8388` published `-p 8388:8388/tcp -p 8388:8388/udp`
|
||||||
- your LAN subnet, i.e. `192.168.1.0/24`, set as `-e EXTRA_SUBNETS=192.168.1.0/24`
|
|
||||||
1. With your Shadowsocks proxy client
|
1. With your Shadowsocks proxy client
|
||||||
- Enter the Docker host (i.e. `192.168.1.10`) as the server IP
|
- Enter the Docker host (i.e. `192.168.1.10`) as the server IP
|
||||||
- Enter port TCP (and UDP, if available) `8388` as the server port
|
- Enter port TCP (and UDP, if available) `8388` as the server port
|
||||||
|
|||||||
@@ -150,6 +150,16 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
|
|||||||
|
|
||||||
firewallConf.SetNetworkInformation(defaultInterface, defaultGateway, localSubnet)
|
firewallConf.SetNetworkInformation(defaultInterface, defaultGateway, localSubnet)
|
||||||
|
|
||||||
|
if err := routingConf.Setup(); err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := routingConf.TearDown(); err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err := ovpnConf.CheckTUN(); err != nil {
|
if err := ovpnConf.CheckTUN(); err != nil {
|
||||||
logger.Warn(err)
|
logger.Warn(err)
|
||||||
err = ovpnConf.CreateTUN()
|
err = ovpnConf.CreateTUN()
|
||||||
@@ -173,12 +183,6 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = firewallConf.SetAllowedSubnets(ctx, allSettings.Firewall.AllowedSubnets)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, vpnPort := range allSettings.Firewall.VPNInputPorts {
|
for _, vpnPort := range allSettings.Firewall.VPNInputPorts {
|
||||||
err = firewallConf.SetAllowedPort(ctx, vpnPort, string(constants.TUN))
|
err = firewallConf.SetAllowedPort(ctx, vpnPort, string(constants.TUN))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,7 +197,7 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
|
|||||||
logger.Error(err)
|
logger.Error(err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
} // TODO move inside firewall?
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,4 @@ services:
|
|||||||
|
|
||||||
# Mullvad only
|
# Mullvad only
|
||||||
- COUNTRY=Sweden
|
- COUNTRY=Sweden
|
||||||
|
|
||||||
# Allow for example your LAN, set to: 192.168.1.0/24
|
|
||||||
- EXTRA_SUBNETS=
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@@ -93,26 +93,12 @@ func (c *configurator) enable(ctx context.Context) (err error) {
|
|||||||
if err = c.acceptOutputThroughInterface(ctx, string(constants.TUN), remove); err != nil {
|
if err = c.acceptOutputThroughInterface(ctx, string(constants.TUN), remove); err != nil {
|
||||||
return fmt.Errorf("cannot enable firewall: %w", err)
|
return fmt.Errorf("cannot enable firewall: %w", err)
|
||||||
}
|
}
|
||||||
if err := c.acceptInputFromSubnetToSubnet(ctx, "*", c.localSubnet, c.localSubnet, remove); err != nil {
|
|
||||||
|
// 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 {
|
||||||
return fmt.Errorf("cannot enable firewall: %w", err)
|
return fmt.Errorf("cannot enable firewall: %w", err)
|
||||||
}
|
}
|
||||||
if err := c.acceptOutputFromSubnetToSubnet(ctx, "*", c.localSubnet, c.localSubnet, remove); err != nil {
|
|
||||||
return fmt.Errorf("cannot enable firewall: %w", err)
|
|
||||||
}
|
|
||||||
for _, subnet := range c.allowedSubnets {
|
|
||||||
if err := c.acceptInputFromSubnetToSubnet(ctx, c.defaultInterface, subnet, c.localSubnet, remove); err != nil {
|
|
||||||
return fmt.Errorf("cannot enable firewall: %w", err)
|
|
||||||
}
|
|
||||||
if err := c.acceptOutputFromSubnetToSubnet(ctx, c.defaultInterface, c.localSubnet, subnet, remove); err != nil {
|
|
||||||
return fmt.Errorf("cannot enable firewall: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Re-ensure all routes exist
|
|
||||||
for _, subnet := range c.allowedSubnets {
|
|
||||||
if err := c.routing.AddRouteVia(subnet, c.defaultGateway, c.defaultInterface); err != nil {
|
|
||||||
return fmt.Errorf("cannot enable firewall: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for port, intf := range c.allowedInputPorts {
|
for port, intf := range c.allowedInputPorts {
|
||||||
if err := c.acceptInputToPort(ctx, intf, port, remove); err != nil {
|
if err := c.acceptInputToPort(ctx, intf, port, remove); err != nil {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ type Configurator interface {
|
|||||||
Version(ctx context.Context) (string, error)
|
Version(ctx context.Context) (string, error)
|
||||||
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)
|
||||||
SetAllowedSubnets(ctx context.Context, subnets []net.IPNet) (err error)
|
|
||||||
SetAllowedPort(ctx context.Context, port uint16, intf string) (err error)
|
SetAllowedPort(ctx context.Context, port uint16, intf string) (err error)
|
||||||
RemoveAllowedPort(ctx context.Context, port uint16) (err error)
|
RemoveAllowedPort(ctx context.Context, port uint16) (err error)
|
||||||
SetDebug()
|
SetDebug()
|
||||||
@@ -40,7 +39,6 @@ type configurator struct { //nolint:maligned
|
|||||||
// State
|
// State
|
||||||
enabled bool
|
enabled bool
|
||||||
vpnConnection models.OpenVPNConnection
|
vpnConnection models.OpenVPNConnection
|
||||||
allowedSubnets []net.IPNet
|
|
||||||
allowedInputPorts map[uint16]string // port to interface mapping
|
allowedInputPorts map[uint16]string // port to interface mapping
|
||||||
stateMutex sync.Mutex
|
stateMutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ func (c *configurator) acceptInputThroughInterface(ctx context.Context, intf str
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func (c *configurator) acceptOutputThroughInterface(ctx context.Context, intf string, remove bool) error {
|
||||||
return c.runIptablesInstruction(ctx, fmt.Sprintf(
|
return c.runIptablesInstruction(ctx, fmt.Sprintf(
|
||||||
"%s OUTPUT -o %s -j ACCEPT", appendOrDelete(remove), intf,
|
"%s OUTPUT -o %s -j ACCEPT", appendOrDelete(remove), intf,
|
||||||
@@ -114,31 +124,6 @@ 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *configurator) acceptInputFromSubnetToSubnet(ctx context.Context,
|
|
||||||
intf string, sourceSubnet, destinationSubnet net.IPNet, remove bool) error {
|
|
||||||
interfaceFlag := "-i " + intf
|
|
||||||
if intf == "*" { // all interfaces
|
|
||||||
interfaceFlag = ""
|
|
||||||
}
|
|
||||||
return c.runIptablesInstruction(ctx, fmt.Sprintf(
|
|
||||||
"%s INPUT %s -s %s -d %s -j ACCEPT",
|
|
||||||
appendOrDelete(remove), interfaceFlag, sourceSubnet.String(), destinationSubnet.String(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thanks to @npawelek.
|
|
||||||
func (c *configurator) acceptOutputFromSubnetToSubnet(ctx context.Context,
|
|
||||||
intf string, sourceSubnet, 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, sourceSubnet.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
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
package firewall
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *configurator) SetAllowedSubnets(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 and updating routes")
|
|
||||||
c.updateSubnetRoutes(c.allowedSubnets, subnets)
|
|
||||||
c.allowedSubnets = make([]net.IPNet, len(subnets))
|
|
||||||
copy(c.allowedSubnets, subnets)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.logger.Info("setting allowed subnets through firewall...")
|
|
||||||
|
|
||||||
subnetsToAdd := findSubnetsToAdd(c.allowedSubnets, subnets)
|
|
||||||
subnetsToRemove := findSubnetsToRemove(c.allowedSubnets, subnets)
|
|
||||||
if len(subnetsToAdd) == 0 && len(subnetsToRemove) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.removeSubnets(ctx, subnetsToRemove, c.defaultInterface, c.localSubnet)
|
|
||||||
if err := c.addSubnets(ctx, subnetsToAdd, c.defaultInterface, c.defaultGateway, c.localSubnet); err != nil {
|
|
||||||
return fmt.Errorf("cannot set allowed subnets through firewall: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *configurator) removeSubnets(ctx context.Context, subnets []net.IPNet, defaultInterface string,
|
|
||||||
localSubnet net.IPNet) {
|
|
||||||
const remove = true
|
|
||||||
for _, subnet := range subnets {
|
|
||||||
failed := false
|
|
||||||
if err := c.acceptInputFromSubnetToSubnet(ctx, defaultInterface, subnet, localSubnet, remove); err != nil {
|
|
||||||
failed = true
|
|
||||||
c.logger.Error("cannot remove outdated allowed subnet through firewall: %s", err)
|
|
||||||
}
|
|
||||||
if err := c.acceptOutputFromSubnetToSubnet(ctx, defaultInterface, subnet, localSubnet, remove); err != nil {
|
|
||||||
failed = true
|
|
||||||
c.logger.Error("cannot remove outdated allowed subnet through firewall: %s", err)
|
|
||||||
}
|
|
||||||
if err := c.routing.DeleteRouteVia(subnet); err != nil {
|
|
||||||
failed = true
|
|
||||||
c.logger.Error("cannot remove outdated allowed subnet route: %s", err)
|
|
||||||
}
|
|
||||||
if failed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
c.allowedSubnets = removeSubnetFromSubnets(c.allowedSubnets, subnet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *configurator) addSubnets(ctx context.Context, subnets []net.IPNet, defaultInterface string,
|
|
||||||
defaultGateway net.IP, localSubnet net.IPNet) error {
|
|
||||||
const remove = false
|
|
||||||
for _, subnet := range subnets {
|
|
||||||
if err := c.acceptInputFromSubnetToSubnet(ctx, defaultInterface, subnet, localSubnet, remove); err != nil {
|
|
||||||
return fmt.Errorf("cannot add allowed subnet through firewall: %w", err)
|
|
||||||
}
|
|
||||||
if err := c.acceptOutputFromSubnetToSubnet(ctx, defaultInterface, localSubnet, subnet, remove); err != nil {
|
|
||||||
return fmt.Errorf("cannot add allowed subnet through firewall: %w", err)
|
|
||||||
}
|
|
||||||
if err := c.routing.AddRouteVia(subnet, defaultGateway, defaultInterface); err != nil {
|
|
||||||
return fmt.Errorf("cannot add route for allowed subnet: %w", err)
|
|
||||||
}
|
|
||||||
c.allowedSubnets = append(c.allowedSubnets, subnet)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateSubnetRoutes does not return an error in order to try to run as many route commands as possible.
|
|
||||||
func (c *configurator) updateSubnetRoutes(oldSubnets, newSubnets []net.IPNet) {
|
|
||||||
subnetsToAdd := findSubnetsToAdd(oldSubnets, newSubnets)
|
|
||||||
subnetsToRemove := findSubnetsToRemove(oldSubnets, newSubnets)
|
|
||||||
if len(subnetsToAdd) == 0 && len(subnetsToRemove) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, subnet := range subnetsToRemove {
|
|
||||||
if err := c.routing.DeleteRouteVia(subnet); err != nil {
|
|
||||||
c.logger.Error("cannot remove outdated route for subnet: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, subnet := range subnetsToAdd {
|
|
||||||
if err := c.routing.AddRouteVia(subnet, c.defaultGateway, c.defaultInterface); err != nil {
|
|
||||||
c.logger.Error("cannot add route for subnet: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package params
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -14,28 +13,6 @@ func (r *reader) GetFirewall() (enabled bool, err error) {
|
|||||||
return r.envParams.GetOnOff("FIREWALL", libparams.Default("on"))
|
return r.envParams.GetOnOff("FIREWALL", libparams.Default("on"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExtraSubnets obtains the CIDR subnets from the comma separated list of the
|
|
||||||
// environment variable EXTRA_SUBNETS.
|
|
||||||
func (r *reader) GetExtraSubnets() (extraSubnets []net.IPNet, err error) {
|
|
||||||
s, err := r.envParams.GetEnv("EXTRA_SUBNETS")
|
|
||||||
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("could not parse subnet %q from environment variable with key EXTRA_SUBNETS: %w", subnet, err)
|
|
||||||
} else if cidr == nil {
|
|
||||||
return nil, fmt.Errorf("parsing subnet %q resulted in a nil CIDR", subnet)
|
|
||||||
}
|
|
||||||
extraSubnets = append(extraSubnets, *cidr)
|
|
||||||
}
|
|
||||||
return extraSubnets, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllowedVPNInputPorts obtains a list of input ports to allow from the
|
// GetAllowedVPNInputPorts obtains a list of input ports to allow from the
|
||||||
// VPN server side in the firewall, from the environment variable FIREWALL_VPN_INPUT_PORTS.
|
// VPN server side in the firewall, from the environment variable FIREWALL_VPN_INPUT_PORTS.
|
||||||
func (r *reader) GetVPNInputPorts() (ports []uint16, err error) {
|
func (r *reader) GetVPNInputPorts() (ports []uint16, err error) {
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ type Reader interface {
|
|||||||
|
|
||||||
// Firewall getters
|
// Firewall getters
|
||||||
GetFirewall() (enabled bool, err error)
|
GetFirewall() (enabled bool, err error)
|
||||||
GetExtraSubnets() (extraSubnets []net.IPNet, err error)
|
|
||||||
GetVPNInputPorts() (ports []uint16, err error)
|
GetVPNInputPorts() (ports []uint16, err error)
|
||||||
GetInputPorts() (ports []uint16, err error)
|
GetInputPorts() (ports []uint16, err error)
|
||||||
GetFirewallDebug() (debug bool, err error)
|
GetFirewallDebug() (debug bool, err error)
|
||||||
|
|||||||
62
internal/routing/enable.go
Normal file
62
internal/routing/enable.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSetup = fmt.Errorf("cannot setup routing")
|
||||||
|
ErrTeardown = fmt.Errorf("cannot teardown routing")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
table = 200
|
||||||
|
priority = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *routing) Setup() (err error) {
|
||||||
|
defaultIP, err := r.defaultIP()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", ErrSetup, err)
|
||||||
|
}
|
||||||
|
defaultInterfaceName, defaultGateway, err := r.DefaultRoute()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", ErrSetup, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.TearDown(); err != nil {
|
||||||
|
r.logger.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := r.addIPRule(defaultIP, table, priority); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", ErrSetup, err)
|
||||||
|
}
|
||||||
|
if err := r.addRouteVia(net.IPNet{}, defaultGateway, defaultInterfaceName, table); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", ErrSetup, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *routing) TearDown() error {
|
||||||
|
defaultIP, err := r.defaultIP()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", ErrTeardown, err)
|
||||||
|
}
|
||||||
|
defaultInterfaceName, defaultGateway, err := r.DefaultRoute()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", ErrTeardown, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.deleteRouteVia(net.IPNet{}, defaultGateway, defaultInterfaceName, table); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", ErrTeardown, err)
|
||||||
|
}
|
||||||
|
if err := r.deleteIPRule(defaultIP, table, priority); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", ErrTeardown, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *routing) AddRouteVia(destination net.IPNet, gateway net.IP, iface string) error {
|
func (r *routing) addRouteVia(destination net.IPNet, gateway net.IP, iface string, table int) error {
|
||||||
destinationStr := destination.String()
|
destinationStr := destination.String()
|
||||||
r.logger.Info("adding route for %s", destinationStr)
|
r.logger.Info("adding route for %s", destinationStr)
|
||||||
if r.debug {
|
if r.debug {
|
||||||
fmt.Printf("ip route add %s via %s dev %s\n", destinationStr, gateway, iface)
|
fmt.Printf("ip route replace %s via %s dev %s table %d\n", destinationStr, gateway, iface, table)
|
||||||
}
|
}
|
||||||
|
|
||||||
link, err := netlink.LinkByName(iface)
|
link, err := netlink.LinkByName(iface)
|
||||||
@@ -22,6 +23,7 @@ func (r *routing) AddRouteVia(destination net.IPNet, gateway net.IP, iface strin
|
|||||||
Dst: &destination,
|
Dst: &destination,
|
||||||
Gw: gateway,
|
Gw: gateway,
|
||||||
LinkIndex: link.Attrs().Index,
|
LinkIndex: link.Attrs().Index,
|
||||||
|
Table: table,
|
||||||
}
|
}
|
||||||
if err := netlink.RouteReplace(&route); err != nil {
|
if err := netlink.RouteReplace(&route); err != nil {
|
||||||
return fmt.Errorf("cannot add route for %s: %w", destinationStr, err)
|
return fmt.Errorf("cannot add route for %s: %w", destinationStr, err)
|
||||||
@@ -29,17 +31,80 @@ func (r *routing) AddRouteVia(destination net.IPNet, gateway net.IP, iface strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *routing) DeleteRouteVia(destination net.IPNet) (err error) {
|
func (r *routing) deleteRouteVia(destination net.IPNet, gateway net.IP, iface string, table int) (err error) {
|
||||||
destinationStr := destination.String()
|
destinationStr := destination.String()
|
||||||
r.logger.Info("deleting route for %s", destinationStr)
|
r.logger.Info("deleting route for %s", destinationStr)
|
||||||
if r.debug {
|
if r.debug {
|
||||||
fmt.Printf("ip route del %s\n", destinationStr)
|
fmt.Printf("ip route delete %s via %s dev %s table %d\n", destinationStr, gateway, iface, table)
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(iface)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot delete route for %s: %w", destinationStr, err)
|
||||||
}
|
}
|
||||||
route := netlink.Route{
|
route := netlink.Route{
|
||||||
Dst: &destination,
|
Dst: &destination,
|
||||||
|
Gw: gateway,
|
||||||
|
LinkIndex: link.Attrs().Index,
|
||||||
|
Table: table,
|
||||||
}
|
}
|
||||||
if err := netlink.RouteDel(&route); err != nil {
|
if err := netlink.RouteDel(&route); err != nil {
|
||||||
return fmt.Errorf("cannot delete route for %s: %w", destinationStr, err)
|
return fmt.Errorf("cannot delete route for %s: %w", destinationStr, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *routing) addIPRule(src net.IP, table, priority int) error {
|
||||||
|
if r.debug {
|
||||||
|
fmt.Printf("ip rule add from %s lookup %d pref %d\n",
|
||||||
|
src, table, priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := netlink.NewRule()
|
||||||
|
rule.Src = netlink.NewIPNet(src)
|
||||||
|
rule.Priority = priority
|
||||||
|
rule.Table = table
|
||||||
|
|
||||||
|
rules, err := netlink.RuleList(netlink.FAMILY_ALL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot add ip rule: %w", err)
|
||||||
|
}
|
||||||
|
for _, existingRule := range rules {
|
||||||
|
if existingRule.Src != nil &&
|
||||||
|
existingRule.Src.IP.Equal(rule.Src.IP) &&
|
||||||
|
bytes.Equal(existingRule.Src.Mask, rule.Src.Mask) &&
|
||||||
|
existingRule.Priority == rule.Priority &&
|
||||||
|
existingRule.Table == rule.Table {
|
||||||
|
return nil // already exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return netlink.RuleAdd(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *routing) deleteIPRule(src net.IP, table, priority int) error {
|
||||||
|
if r.debug {
|
||||||
|
fmt.Printf("ip rule del from %s lookup %d pref %d\n",
|
||||||
|
src, table, priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := netlink.NewRule()
|
||||||
|
rule.Src = netlink.NewIPNet(src)
|
||||||
|
rule.Priority = priority
|
||||||
|
rule.Table = table
|
||||||
|
|
||||||
|
rules, err := netlink.RuleList(netlink.FAMILY_ALL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot add ip rule: %w", err)
|
||||||
|
}
|
||||||
|
for _, existingRule := range rules {
|
||||||
|
if existingRule.Src != nil &&
|
||||||
|
existingRule.Src.IP.Equal(rule.Src.IP) &&
|
||||||
|
bytes.Equal(existingRule.Src.Mask, rule.Src.Mask) &&
|
||||||
|
existingRule.Priority == rule.Priority &&
|
||||||
|
existingRule.Table == rule.Table {
|
||||||
|
return netlink.RuleDel(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,30 @@ 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) {
|
||||||
|
routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get default IP address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultLinkName := ""
|
||||||
|
for _, route := range routes {
|
||||||
|
if route.Dst == nil {
|
||||||
|
linkIndex := route.LinkIndex
|
||||||
|
link, err := netlink.LinkByIndex(linkIndex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get default IP address: %w", err)
|
||||||
|
}
|
||||||
|
defaultLinkName = link.Attrs().Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(defaultLinkName) == 0 {
|
||||||
|
return nil, fmt.Errorf("cannot find default link name in %d routes", len(routes))
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.assignedIP(defaultLinkName)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *routing) LocalSubnet() (defaultSubnet net.IPNet, err error) {
|
func (r *routing) LocalSubnet() (defaultSubnet net.IPNet, err error) {
|
||||||
routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
|
routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -60,6 +84,26 @@ func (r *routing) LocalSubnet() (defaultSubnet net.IPNet, err error) {
|
|||||||
return defaultSubnet, fmt.Errorf("cannot find default subnet in %d routes", len(routes))
|
return defaultSubnet, fmt.Errorf("cannot find default subnet in %d routes", len(routes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *routing) assignedIP(interfaceName string) (ip net.IP, err error) {
|
||||||
|
iface, err := net.InterfaceByName(interfaceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addresses, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, address := range addresses {
|
||||||
|
switch value := address.(type) {
|
||||||
|
case *net.IPAddr:
|
||||||
|
return value.IP, nil
|
||||||
|
case *net.IPNet:
|
||||||
|
return value.IP, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("IP address not found in addresses of interface %s", interfaceName)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *routing) VPNDestinationIP() (ip net.IP, err error) {
|
func (r *routing) VPNDestinationIP() (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 {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Routing interface {
|
type Routing interface {
|
||||||
AddRouteVia(destination net.IPNet, gateway net.IP, iface string) error
|
Setup() (err error)
|
||||||
DeleteRouteVia(destination net.IPNet) (err error)
|
TearDown() error
|
||||||
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)
|
||||||
VPNDestinationIP() (ip net.IP, err error)
|
VPNDestinationIP() (ip net.IP, err error)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/params"
|
"github.com/qdm12/gluetun/internal/params"
|
||||||
@@ -10,7 +9,6 @@ import (
|
|||||||
|
|
||||||
// Firewall contains settings to customize the firewall operation.
|
// Firewall contains settings to customize the firewall operation.
|
||||||
type Firewall struct {
|
type Firewall struct {
|
||||||
AllowedSubnets []net.IPNet
|
|
||||||
VPNInputPorts []uint16
|
VPNInputPorts []uint16
|
||||||
InputPorts []uint16
|
InputPorts []uint16
|
||||||
Enabled bool
|
Enabled bool
|
||||||
@@ -18,10 +16,6 @@ type Firewall struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Firewall) String() string {
|
func (f *Firewall) String() string {
|
||||||
allowedSubnets := make([]string, len(f.AllowedSubnets))
|
|
||||||
for i := range f.AllowedSubnets {
|
|
||||||
allowedSubnets[i] = f.AllowedSubnets[i].String()
|
|
||||||
}
|
|
||||||
if !f.Enabled {
|
if !f.Enabled {
|
||||||
return "Firewall settings: disabled"
|
return "Firewall settings: disabled"
|
||||||
}
|
}
|
||||||
@@ -36,7 +30,6 @@ func (f *Firewall) String() string {
|
|||||||
|
|
||||||
settingsList := []string{
|
settingsList := []string{
|
||||||
"Firewall settings:",
|
"Firewall settings:",
|
||||||
"Allowed subnets: " + strings.Join(allowedSubnets, ", "),
|
|
||||||
"VPN input ports: " + strings.Join(vpnInputPorts, ", "),
|
"VPN input ports: " + strings.Join(vpnInputPorts, ", "),
|
||||||
"Input ports: " + strings.Join(inputPorts, ", "),
|
"Input ports: " + strings.Join(inputPorts, ", "),
|
||||||
}
|
}
|
||||||
@@ -48,10 +41,6 @@ func (f *Firewall) String() string {
|
|||||||
|
|
||||||
// GetFirewallSettings obtains firewall settings from environment variables using the params package.
|
// GetFirewallSettings obtains firewall settings from environment variables using the params package.
|
||||||
func GetFirewallSettings(paramsReader params.Reader) (settings Firewall, err error) {
|
func GetFirewallSettings(paramsReader params.Reader) (settings Firewall, err error) {
|
||||||
settings.AllowedSubnets, err = paramsReader.GetExtraSubnets()
|
|
||||||
if err != nil {
|
|
||||||
return settings, err
|
|
||||||
}
|
|
||||||
settings.VPNInputPorts, err = paramsReader.GetVPNInputPorts()
|
settings.VPNInputPorts, err = paramsReader.GetVPNInputPorts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return settings, err
|
return settings, err
|
||||||
|
|||||||
Reference in New Issue
Block a user