Files
tsnet-proxy/vendor/tailscale.com/ipn/ipnlocal/via.go
T
2026-10-22 12:27:19 +01:00

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
}