From aa88ab78f10dd5aa3085bee855555fd6cdc5d991 Mon Sep 17 00:00:00 2001 From: DasSkelett Date: Thu, 17 Mar 2022 17:01:11 +0100 Subject: [PATCH] Add optional client isolation feature for blocking traffic between devices Until now there was absolutely no firewall between all online devices. While this is not problem (and often even wanted behaviour) for single-user setups, it can be useful for multi-user situations. --- cmd/serve/main.go | 17 +++++++-- docs/2-configuration.md | 2 ++ internal/config/config.go | 25 +++++++------ internal/network/network.go | 70 +++++++++++++++++++++++++------------ 4 files changed, 78 insertions(+), 36 deletions(-) diff --git a/cmd/serve/main.go b/cmd/serve/main.go index dbf349c9..2930e389 100644 --- a/cmd/serve/main.go +++ b/cmd/serve/main.go @@ -49,12 +49,13 @@ func Register(app *kingpin.Application) *servecmd { cli.Flag("wireguard-interface", "Set the wireguard interface name").Default("wg0").Envar("WG_WIREGUARD_INTERFACE").StringVar(&cmd.AppConfig.WireGuard.Interface) cli.Flag("wireguard-private-key", "Wireguard private key").Envar("WG_WIREGUARD_PRIVATE_KEY").StringVar(&cmd.AppConfig.WireGuard.PrivateKey) cli.Flag("wireguard-port", "The port that the Wireguard server will listen on").Envar("WG_WIREGUARD_PORT").Default("51820").IntVar(&cmd.AppConfig.WireGuard.Port) + cli.Flag("vpn-allowed-ips", "A list of networks that VPN clients will be allowed to connect to via the VPN").Envar("WG_VPN_ALLOWED_IPS").Default("0.0.0.0/0", "::/0").StringsVar(&cmd.AppConfig.VPN.AllowedIPs) cli.Flag("vpn-cidr", "The network CIDR for the VPN").Envar("WG_VPN_CIDR").Default("10.44.0.0/24").StringVar(&cmd.AppConfig.VPN.CIDR) cli.Flag("vpn-cidrv6", "The IPv6 network CIDR for the VPN").Envar("WG_VPN_CIDRV6").Default("fd48:4c4:7aa9::/64").StringVar(&cmd.AppConfig.VPN.CIDRv6) + cli.Flag("vpn-gateway-interface", "The gateway network interface (i.e. eth0)").Envar("WG_VPN_GATEWAY_INTERFACE").Default(detectDefaultInterface()).StringVar(&cmd.AppConfig.VPN.GatewayInterface) cli.Flag("vpn-nat44-enabled", "Enable or disable NAT of IPv6 traffic leaving through the gateway").Envar("WG_IPV4_NAT_ENABLED").Default("true").BoolVar(&cmd.AppConfig.VPN.NAT44) cli.Flag("vpn-nat66-enabled", "Enable or disable NAT of IPv6 traffic leaving through the gateway").Envar("WG_IPV6_NAT_ENABLED").Default("true").BoolVar(&cmd.AppConfig.VPN.NAT66) - cli.Flag("vpn-gateway-interface", "The gateway network interface (i.e. eth0)").Envar("WG_VPN_GATEWAY_INTERFACE").Default(detectDefaultInterface()).StringVar(&cmd.AppConfig.VPN.GatewayInterface) - cli.Flag("vpn-allowed-ips", "A list of networks that VPN clients will be allowed to connect to via the VPN").Envar("WG_VPN_ALLOWED_IPS").Default("0.0.0.0/0", "::/0").StringsVar(&cmd.AppConfig.VPN.AllowedIPs) + cli.Flag("vpn-client-isolation", "Block or allow traffic between client devices").Envar("WG_VPN_CLIENT_ISOLATION").Default("false").BoolVar(&cmd.AppConfig.VPN.ClientIsolation) cli.Flag("dns-enabled", "Enable or disable the embedded dns proxy server (useful for development)").Envar("WG_DNS_ENABLED").Default("true").BoolVar(&cmd.AppConfig.DNS.Enabled) cli.Flag("dns-upstream", "An upstream DNS server to proxy DNS traffic to. Defaults to resolvconf with Cloudflare DNS as fallback").Envar("WG_DNS_UPSTREAM").StringsVar(&cmd.AppConfig.DNS.Upstream) cli.Flag("dns-domain", "A domain to serve configured device names authoritatively").Envar("WG_DNS_DOMAIN").StringVar(&cmd.AppConfig.DNS.Domain) @@ -141,7 +142,17 @@ func (cmd *servecmd) Run() { logrus.Infof("wireguard VPN network is %s", network.StringJoinIPNets(vpnip, vpnipv6)) - if err := network.ConfigureForwarding(conf.VPN.GatewayInterface, conf.VPN.CIDR, conf.VPN.CIDRv6, conf.VPN.NAT44, conf.VPN.NAT66, conf.VPN.AllowedIPs); err != nil { + options := network.ForwardingOptions{ + GatewayIface: conf.VPN.GatewayInterface, + CIDR: conf.VPN.CIDR, + CIDRv6: conf.VPN.CIDRv6, + NAT44: conf.VPN.NAT44, + NAT66: conf.VPN.NAT66, + ClientIsolation: conf.VPN.ClientIsolation, + AllowedIPs: conf.VPN.AllowedIPs, + } + + if err := network.ConfigureForwarding(options); err != nil { logrus.Error(err) return } diff --git a/docs/2-configuration.md b/docs/2-configuration.md index 8fedbbf1..79ace67a 100644 --- a/docs/2-configuration.md +++ b/docs/2-configuration.md @@ -36,6 +36,7 @@ Here's what you can configure: | `WG_VPN_CIDR` | `--vpn-cidr` | `vpn.cidr` | | `10.44.0.0/24` | The VPN IPv4 network range. VPN clients will be assigned IP addresses in this range. Set to `0` to disable IPv4. | | `WG_IPV4_NAT_ENABLED` | `--vpn-nat44-enabled` | `vpn.nat44` | | `true` | Disables NAT for IPv4 | | `WG_IPV6_NAT_ENABLED` | `--vpn-nat66-enabled` | `vpn.nat66` | | `true` | Disables NAT for IPv6 | +| `WG_VPN_CLIENT_ISOLATION` | `--vpn-client-isolation` | `vpn.clientIsolation` | | `false` | BLock or allow traffic between client devices (client isolation) | | `WG_VPN_CIDRV6` | `--vpn-cidrv6` | `vpn.cidrv6` | | `fd48:4c4:7aa9::/64` | The VPN IPv6 network range. VPN clients will be assigned IP addresses in this range. Set to `0` to disable IPv6. | | `WG_VPN_GATEWAY_INTERFACE` | `--vpn-gateway-interface` | `vpn.gatewayInterface` | | _default gateway interface (e.g. eth0)_ | The VPN gateway interface. VPN client traffic will be forwarded to this interface. | | `WG_VPN_ALLOWED_IPS` | `--vpn-allowed-ips` | `vpn.allowedIPs` | | `0.0.0.0/0, ::/0` | Allowed IPs that clients may route through this VPN. This will be set in the client's WireGuard connection file and routing is also enforced by the server using iptables. | @@ -54,5 +55,6 @@ wireguard: privateKey: "" dns: upstream: + - "2001:4860:4860::8888" - "8.8.8.8" ``` diff --git a/internal/config/config.go b/internal/config/config.go index 5c5ed368..b5a41aa7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -54,6 +54,12 @@ type AppConfig struct { } `yaml:"wireguard"` // Configure VPN related settings (networking) VPN struct { + // The "AllowedIPs" for VPN clients. + // This value will be included in client config + // files and in server-side iptable rules + // to enforce network access. + // defaults to ["0.0.0.0/0", "::/0"] + AllowedIPs []string `yaml:"allowedIPs"` // CIDR configures a network address space // that client (WireGuard peers) will be allocated // an IP address from @@ -64,6 +70,11 @@ type AppConfig struct { // an IP address from // defaults to fd48:4c4:7aa9::/64 CIDRv6 string `yaml:"cidrv6"` + // GatewayInterface will be used in iptable forwarding + // rules that send VPN traffic from clients to this interface + // Most use-cases will want this interface to have access + // to the outside internet + GatewayInterface string `yaml:"gatewayInterface"` // NAT44 configures whether IPv4 traffic leaving // through the GatewayInterface should be masqueraded // defaults to true @@ -73,17 +84,9 @@ type AppConfig struct { // masqueraded like IPv4 traffic // defaults to true NAT66 bool `yaml:"nat66"` - // GatewayInterface will be used in iptable forwarding - // rules that send VPN traffic from clients to this interface - // Most use-cases will want this interface to have access - // to the outside internet - GatewayInterface string `yaml:"gatewayInterface"` - // The "AllowedIPs" for VPN clients. - // This value will be included in client config - // files and in server-side iptable rules - // to enforce network access. - // defaults to ["0.0.0.0/0", "::/0"] - AllowedIPs []string `yaml:"allowedIPs"` + // ClientIsolation configures whether traffic between client devices will be blocked or allowed + // defaults to false + ClientIsolation bool `yaml:"clientIsolation"` } `yaml:"vpn"` // Configure the embedded DNS server DNS struct { diff --git a/internal/network/network.go b/internal/network/network.go index a4ee4bf3..b7bbdcae 100644 --- a/internal/network/network.go +++ b/internal/network/network.go @@ -65,14 +65,25 @@ func SplitAddresses(addresses string) []string { return split } -func ConfigureForwarding(gatewayIface string, cidr, cidrv6 string, nat44, nat66 bool, allowedIPs []string) error { +// ForwardingOptions contains all options used for configuring the firewall rules +type ForwardingOptions struct { + GatewayIface string + CIDR, CIDRv6 string + NAT44, NAT66 bool + ClientIsolation bool + AllowedIPs []string + allowedIPv4s []string + allowedIPv6s []string +} + +func ConfigureForwarding(options ForwardingOptions) error { // Networking configuration (iptables) configuration // to ensure that traffic from clients of the wireguard interface // is sent to the provided network interface - allowedIPv4s := make([]string, 0, len(allowedIPs)/2) - allowedIPv6s := make([]string, 0, len(allowedIPs)/2) + allowedIPv4s := make([]string, 0, len(options.AllowedIPs)/2) + allowedIPv6s := make([]string, 0, len(options.AllowedIPs)/2) - for _, allowedCIDR := range allowedIPs { + for _, allowedCIDR := range options.AllowedIPs { parsedAddress, parsedNetwork, err := net.ParseCIDR(allowedCIDR) if err != nil { return errors.Wrap(err, "invalid cidr in AllowedIPs") @@ -86,21 +97,23 @@ func ConfigureForwarding(gatewayIface string, cidr, cidrv6 string, nat44, nat66 allowedIPv6s = append(allowedIPv6s, parsedNetwork.String()) } } + options.allowedIPv4s = allowedIPv4s + options.allowedIPv6s = allowedIPv6s - if cidr != "" { - if err := configureForwardingv4(gatewayIface, cidr, nat44, allowedIPv4s); err != nil { + if options.CIDR != "" { + if err := configureForwardingv4(options); err != nil { return err } } - if cidrv6 != "" { - if err := configureForwardingv6(gatewayIface, cidrv6, nat66, allowedIPv6s); err != nil { + if options.CIDRv6 != "" { + if err := configureForwardingv6(options); err != nil { return err } } return nil } -func configureForwardingv4(gatewayIface string, cidr string, nat44 bool, allowedIPs []string) error { +func configureForwardingv4(options ForwardingOptions) error { ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) if err != nil { return errors.Wrap(err, "failed to init iptables") @@ -128,19 +141,26 @@ func configureForwardingv4(gatewayIface string, cidr string, nat44 bool, allowed return errors.Wrap(err, "failed to append POSTROUTING rule to nat chain") } - for _, allowedCIDR := range allowedIPs { - if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidr, "-d", allowedCIDR, "-j", "ACCEPT"); err != nil { + if options.ClientIsolation { + // Reject inter-device traffic + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", options.CIDR, "-d", options.CIDR, "-j", "REJECT"); err != nil { + return errors.Wrap(err, "failed to set ip tables rule") + } + } + // Accept client traffic for given allowed ips + for _, allowedCIDR := range options.allowedIPv4s { + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", options.CIDR, "-d", allowedCIDR, "-j", "ACCEPT"); err != nil { return errors.Wrap(err, "failed to set ip tables rule") } } // And reject everything else - if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidr, "-j", "REJECT"); err != nil { + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", options.CIDR, "-j", "REJECT"); err != nil { return errors.Wrap(err, "failed to set ip tables rule") } - if gatewayIface != "" { - if nat44 { - if err := ipt.AppendUnique("nat", "WG_ACCESS_SERVER_POSTROUTING", "-s", cidr, "-o", gatewayIface, "-j", "MASQUERADE"); err != nil { + if options.GatewayIface != "" { + if options.NAT44 { + if err := ipt.AppendUnique("nat", "WG_ACCESS_SERVER_POSTROUTING", "-s", options.CIDR, "-o", options.GatewayIface, "-j", "MASQUERADE"); err != nil { return errors.Wrap(err, "failed to set ip tables rule") } } @@ -148,7 +168,7 @@ func configureForwardingv4(gatewayIface string, cidr string, nat44 bool, allowed return nil } -func configureForwardingv6(gatewayIface string, cidrv6 string, nat66 bool, allowedIPs []string) error { +func configureForwardingv6(options ForwardingOptions) error { ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv6) if err != nil { return errors.Wrap(err, "failed to init ip6tables") @@ -174,20 +194,26 @@ func configureForwardingv6(gatewayIface string, cidrv6 string, nat66 bool, allow return errors.Wrap(err, "failed to append POSTROUTING rule to nat chain") } + if options.ClientIsolation { + // Reject inter-device traffic + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", options.CIDRv6, "-d", options.CIDRv6, "-j", "REJECT"); err != nil { + return errors.Wrap(err, "failed to set ip tables rule") + } + } // Accept client traffic for given allowed ips - for _, allowedCIDR := range allowedIPs { - if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidrv6, "-d", allowedCIDR, "-j", "ACCEPT"); err != nil { + for _, allowedCIDR := range options.allowedIPv6s { + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", options.CIDRv6, "-d", allowedCIDR, "-j", "ACCEPT"); err != nil { return errors.Wrap(err, "failed to set ip tables rule") } } // And reject everything else - if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidrv6, "-j", "REJECT"); err != nil { + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", options.CIDRv6, "-j", "REJECT"); err != nil { return errors.Wrap(err, "failed to set ip tables rule") } - if gatewayIface != "" { - if nat66 { - if err := ipt.AppendUnique("nat", "WG_ACCESS_SERVER_POSTROUTING", "-s", cidrv6, "-o", gatewayIface, "-j", "MASQUERADE"); err != nil { + if options.GatewayIface != "" { + if options.NAT66 { + if err := ipt.AppendUnique("nat", "WG_ACCESS_SERVER_POSTROUTING", "-s", options.CIDRv6, "-o", options.GatewayIface, "-j", "MASQUERADE"); err != nil { return errors.Wrap(err, "failed to set ip tables rule") } }