135 lines
3.7 KiB
Go
135 lines
3.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/netip"
|
|
"strings"
|
|
"sync"
|
|
|
|
"go4.org/netipx"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/util/clientmetric"
|
|
)
|
|
|
|
// v4BroadcastAddr is 255.255.255.255, the limited broadcast address.
|
|
var v4BroadcastAddr = netip.AddrFrom4([4]byte{255, 255, 255, 255})
|
|
|
|
// viaAdditionsEnv is the name of an environment variable that can be set to
|
|
// override the 4via6 host-scoped address restrictions. Only use this if you are
|
|
// certain that there is not a better solution (e.g. perhaps tailscale serve,
|
|
// possibly using a Service). Use of this feature will have security
|
|
// implications: ensure that your ACLs/Grants are written appropriately.
|
|
// The value of the environment variable is a comma separated list of IPv4
|
|
// ranges (two IPv4 addresses separated by a hyphen), IPv4 prefixes (CIDR
|
|
// notation), or IPv4 addresses. Spaces are permitted around the commas.
|
|
const viaAdditionsEnv = "TS_4VIA6_ALLOW_LOCAL"
|
|
|
|
var viaTargetAdditions = sync.OnceValue(func() *netipx.IPSet {
|
|
v, err := generateViaTargetAdditions(envknob.String(viaAdditionsEnv))
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
|
|
if v != nil {
|
|
has4via6AdditionsMetric := clientmetric.NewGauge("4via6_allow_local")
|
|
has4via6AdditionsMetric.Set(1)
|
|
}
|
|
|
|
return v
|
|
})
|
|
|
|
func generateViaTargetAdditions(rawAdditions string) (*netipx.IPSet, error) {
|
|
rawAdditions = strings.TrimSpace(rawAdditions)
|
|
if rawAdditions == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
var ips netipx.IPSetBuilder
|
|
|
|
rawAddrs := strings.Split(rawAdditions, ",")
|
|
errs := make([]error, 0, len(rawAddrs))
|
|
for _, rawAddr := range rawAddrs {
|
|
rawAddr = strings.TrimSpace(rawAddr)
|
|
if rawAddr == "" {
|
|
continue
|
|
}
|
|
|
|
// Try parsing as a range
|
|
if ipRange, err := netipx.ParseIPRange(rawAddr); err == nil {
|
|
if !ipRange.From().Is4() || !ipRange.To().Is4() {
|
|
errs = append(errs, fmt.Errorf("rejecting %v range %v: not IPv4", viaAdditionsEnv, ipRange))
|
|
continue
|
|
}
|
|
|
|
ips.AddRange(ipRange)
|
|
continue
|
|
}
|
|
|
|
// Try parsing as a prefix
|
|
if prefix, err := netip.ParsePrefix(rawAddr); err == nil {
|
|
if !prefix.Addr().Is4() {
|
|
errs = append(errs, fmt.Errorf("rejecting %v prefix %v: not IPv4", viaAdditionsEnv, prefix))
|
|
continue
|
|
}
|
|
|
|
ips.AddPrefix(prefix)
|
|
continue
|
|
}
|
|
|
|
// Try parsing as an individual IP
|
|
if addr, err := netip.ParseAddr(rawAddr); err == nil {
|
|
if !addr.Is4() {
|
|
errs = append(errs, fmt.Errorf("rejecting %v address %v: not IPv4", viaAdditionsEnv, addr))
|
|
continue
|
|
}
|
|
|
|
ips.Add(addr)
|
|
continue
|
|
}
|
|
|
|
errs = append(errs, fmt.Errorf("rejecting %v value %q: not a range, prefix, or address", viaAdditionsEnv, rawAddr))
|
|
}
|
|
|
|
ipSet, err := ips.IPSet()
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
return ipSet, errors.Join(errs...)
|
|
}
|
|
|
|
// viaTargetAllowed reports whether ip may be forwarded to after unmapping a
|
|
// 4via6 destination. The packet filter only sees the outer via address, so
|
|
// this is the sole check on the embedded IPv4 target.
|
|
func viaTargetAllowed(ip netip.Addr) bool {
|
|
if !ip.Is4() {
|
|
return false // UnmapVia only returns IPv4
|
|
}
|
|
if additions := viaTargetAdditions(); additions != nil {
|
|
// Permit unusual configurations to use host-scoped IP addresses as
|
|
// 4via6 destinations when provided in an envknob.
|
|
if additions.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
if ip.IsLoopback() || ip.IsMulticast() || ip.IsUnspecified() || ip == v4BroadcastAddr {
|
|
return false
|
|
}
|
|
if tsaddr.IsTailscaleIP(ip) {
|
|
// A CGNAT-range target may route to tailscale0 or to a site LAN
|
|
// depending on the tailnet and OS (see shouldUseOneCGNATRoute);
|
|
// it's hard to detect when forwarding would be OK, so deny always
|
|
return false
|
|
}
|
|
if ip.IsLinkLocalUnicast() {
|
|
return false
|
|
}
|
|
return true
|
|
}
|