util/linuxfw: add container-friendly IPv6 NAT check (#11353)
Remove IPv6 NAT check when routing is being set up using nftables. This is unnecessary as support for nftables was added after support for IPv6. https://tldp.org/HOWTO/Linux+IPv6-HOWTO/ch18s04.html https://wiki.nftables.org/wiki-nftables/index.php/Building_and_installing_nftables_from_sources Additionally, run an extra check for IPv6 NAT support when the routing is set up with iptables. This is because the earlier checks rely on being able to use modprobe and on /proc/net/ip6_tables_names being populated on start - these conditions are usually not true in container environments. Updates tailscale/tailscale#11344 Signed-off-by: Irbe Krumina <irbe@tailscale.com>
This commit is contained in:
parent
fd942b5384
commit
90c4067010
|
@ -6,8 +6,10 @@
|
||||||
package linuxfw
|
package linuxfw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -59,6 +61,7 @@ func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
|
||||||
supportsV6, supportsV6NAT := false, false
|
supportsV6, supportsV6NAT := false, false
|
||||||
v6err := checkIPv6(logf)
|
v6err := checkIPv6(logf)
|
||||||
ip6terr := checkIP6TablesExists()
|
ip6terr := checkIP6TablesExists()
|
||||||
|
var ipt6 *iptables.IPTables
|
||||||
switch {
|
switch {
|
||||||
case v6err != nil:
|
case v6err != nil:
|
||||||
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
|
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
|
||||||
|
@ -66,20 +69,54 @@ func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
|
||||||
logf("disabling tunneled IPv6 due to missing ip6tables: %v", ip6terr)
|
logf("disabling tunneled IPv6 due to missing ip6tables: %v", ip6terr)
|
||||||
default:
|
default:
|
||||||
supportsV6 = true
|
supportsV6 = true
|
||||||
supportsV6NAT = supportsV6 && checkSupportsV6NAT()
|
|
||||||
logf("v6nat = %v", supportsV6NAT)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ipt6 *iptables.IPTables
|
|
||||||
if supportsV6 {
|
|
||||||
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
supportsV6NAT = checkSupportsV6NAT(ipt6, logf)
|
||||||
|
logf("v6nat = %v", supportsV6NAT)
|
||||||
}
|
}
|
||||||
return &iptablesRunner{ipt4, ipt6, supportsV6, supportsV6NAT}, nil
|
return &iptablesRunner{ipt4, ipt6, supportsV6, supportsV6NAT}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkSupportsV6NAT returns whether the system has a "nat" table in the
|
||||||
|
// IPv6 netfilter stack.
|
||||||
|
//
|
||||||
|
// The nat table was added after the initial release of ipv6
|
||||||
|
// netfilter, so some older distros ship a kernel that can't NAT IPv6
|
||||||
|
// traffic.
|
||||||
|
// ipt must be initialized for IPv6.
|
||||||
|
func checkSupportsV6NAT(ipt *iptables.IPTables, logf logger.Logf) bool {
|
||||||
|
if ipt == nil || ipt.Proto() != iptables.ProtocolIPv6 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
natListErr, _ := ipt.ListChains("nat")
|
||||||
|
if natListErr == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO (irbekrm): the following two checks were added before the check
|
||||||
|
// above that verifies that nat chains can be listed. It is a
|
||||||
|
// container-friendly check (see
|
||||||
|
// https://github.com/tailscale/tailscale/issues/11344), but also should
|
||||||
|
// be good enough on its own in other environments. If we never observe
|
||||||
|
// it falsely succeed, let's remove the other two checks.
|
||||||
|
|
||||||
|
bs, err := os.ReadFile("/proc/net/ip6_tables_names")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if bytes.Contains(bs, []byte("nat\n")) {
|
||||||
|
logf("[unexpected] listing nat chains failed, but /proc/net/ip6_tables_name reports a nat table existing")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if exec.Command("modprobe", "ip6table_nat").Run() == nil {
|
||||||
|
logf("[unexpected] listing nat chains failed, but modprobe ip6table_nat succeeded")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// HasIPV6 reports true if the system supports IPv6.
|
// HasIPV6 reports true if the system supports IPv6.
|
||||||
func (i *iptablesRunner) HasIPV6() bool {
|
func (i *iptablesRunner) HasIPV6() bool {
|
||||||
return i.v6Available
|
return i.v6Available
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
package linuxfw
|
package linuxfw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
@ -170,28 +169,6 @@ func checkIPv6(logf logger.Logf) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkSupportsV6NAT returns whether the system has a "nat" table in the
|
|
||||||
// IPv6 netfilter stack.
|
|
||||||
//
|
|
||||||
// The nat table was added after the initial release of ipv6
|
|
||||||
// netfilter, so some older distros ship a kernel that can't NAT IPv6
|
|
||||||
// traffic.
|
|
||||||
func checkSupportsV6NAT() bool {
|
|
||||||
bs, err := os.ReadFile("/proc/net/ip6_tables_names")
|
|
||||||
if err != nil {
|
|
||||||
// Can't read the file. Assume SNAT works.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if bytes.Contains(bs, []byte("nat\n")) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// In nftables mode, that proc file will be empty. Try another thing:
|
|
||||||
if exec.Command("modprobe", "ip6table_nat").Run() == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckIPRuleSupportsV6(logf logger.Logf) error {
|
func CheckIPRuleSupportsV6(logf logger.Logf) error {
|
||||||
// First try just a read-only operation to ideally avoid
|
// First try just a read-only operation to ideally avoid
|
||||||
// having to modify any state.
|
// having to modify any state.
|
||||||
|
|
|
@ -551,12 +551,15 @@ func newNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) {
|
||||||
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
|
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
|
||||||
}
|
}
|
||||||
supportsV6 := v6err == nil
|
supportsV6 := v6err == nil
|
||||||
supportsV6NAT := supportsV6 && checkSupportsV6NAT()
|
|
||||||
|
|
||||||
var nft6 *nftable
|
var nft6 *nftable
|
||||||
|
|
||||||
if supportsV6 {
|
if supportsV6 {
|
||||||
logf("v6nat availability: %v", supportsV6NAT)
|
|
||||||
nft6 = &nftable{Proto: nftables.TableFamilyIPv6}
|
nft6 = &nftable{Proto: nftables.TableFamilyIPv6}
|
||||||
|
// Kernel support for nftables was added after support for IPv6
|
||||||
|
// NAT, so no need for a separate IPv6 NAT support check.
|
||||||
|
// https://tldp.org/HOWTO/Linux+IPv6-HOWTO/ch18s04.html
|
||||||
|
// https://wiki.nftables.org/wiki-nftables/index.php/Building_and_installing_nftables_from_sources
|
||||||
|
logf("v6nat availability: true")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(KevinLiang10): convert iptables rule to nftable rules if they exist in the iptables
|
// TODO(KevinLiang10): convert iptables rule to nftable rules if they exist in the iptables
|
||||||
|
@ -566,7 +569,7 @@ func newNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) {
|
||||||
nft4: nft4,
|
nft4: nft4,
|
||||||
nft6: nft6,
|
nft6: nft6,
|
||||||
v6Available: supportsV6,
|
v6Available: supportsV6,
|
||||||
v6NATAvailable: supportsV6NAT,
|
v6NATAvailable: supportsV6, // if nftables are supported, IPv6 NAT is supported
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue