@ -16,6 +16,8 @@
// - TS_ROUTES: subnet routes to advertise.
// - TS_ROUTES: subnet routes to advertise.
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
// destination.
// destination.
// - TS_TAILNET_TARGET_IP: proxy all incoming non-Tailscale traffic to the given
// destination.
// - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'.
// - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'.
// - TS_EXTRA_ARGS: extra arguments to 'tailscale login', these are not
// - TS_EXTRA_ARGS: extra arguments to 'tailscale login', these are not
// reset on restart.
// reset on restart.
@ -88,8 +90,9 @@ func main() {
AuthKey : defaultEnvs ( [ ] string { "TS_AUTHKEY" , "TS_AUTH_KEY" } , "" ) ,
AuthKey : defaultEnvs ( [ ] string { "TS_AUTHKEY" , "TS_AUTH_KEY" } , "" ) ,
Hostname : defaultEnv ( "TS_HOSTNAME" , "" ) ,
Hostname : defaultEnv ( "TS_HOSTNAME" , "" ) ,
Routes : defaultEnv ( "TS_ROUTES" , "" ) ,
Routes : defaultEnv ( "TS_ROUTES" , "" ) ,
ProxyTo : defaultEnv ( "TS_DEST_IP" , "" ) ,
ServeConfigPath : defaultEnv ( "TS_SERVE_CONFIG" , "" ) ,
ServeConfigPath : defaultEnv ( "TS_SERVE_CONFIG" , "" ) ,
ProxyTo : defaultEnv ( "TS_DEST_IP" , "" ) ,
TailnetTargetIP : defaultEnv ( "TS_TAILNET_TARGET_IP" , "" ) ,
DaemonExtraArgs : defaultEnv ( "TS_TAILSCALED_EXTRA_ARGS" , "" ) ,
DaemonExtraArgs : defaultEnv ( "TS_TAILSCALED_EXTRA_ARGS" , "" ) ,
ExtraArgs : defaultEnv ( "TS_EXTRA_ARGS" , "" ) ,
ExtraArgs : defaultEnv ( "TS_EXTRA_ARGS" , "" ) ,
InKubernetes : os . Getenv ( "KUBERNETES_SERVICE_HOST" ) != "" ,
InKubernetes : os . Getenv ( "KUBERNETES_SERVICE_HOST" ) != "" ,
@ -107,16 +110,17 @@ func main() {
if cfg . ProxyTo != "" && cfg . UserspaceMode {
if cfg . ProxyTo != "" && cfg . UserspaceMode {
log . Fatal ( "TS_DEST_IP is not supported with TS_USERSPACE" )
log . Fatal ( "TS_DEST_IP is not supported with TS_USERSPACE" )
}
}
if cfg . ProxyTo != "" && cfg . ServeConfigPath != "" {
log . Fatal ( "TS_DEST_IP is not supported with TS_SERVE_CONFIG" )
if cfg . TailnetTargetIP != "" && cfg . UserspaceMode {
log . Fatal ( "TS_TAILNET_TARGET_IP is not supported with TS_USERSPACE" )
}
}
if ! cfg . UserspaceMode {
if ! cfg . UserspaceMode {
if err := ensureTunFile ( cfg . Root ) ; err != nil {
if err := ensureTunFile ( cfg . Root ) ; err != nil {
log . Fatalf ( "Unable to create tuntap device file: %v" , err )
log . Fatalf ( "Unable to create tuntap device file: %v" , err )
}
}
if cfg . ProxyTo != "" || cfg . Routes != "" {
if cfg . ProxyTo != "" || cfg . Routes != "" || cfg . TailnetTargetIP != "" {
if err := ensureIPForwarding ( cfg . Root , cfg . ProxyTo , cfg . Routes) ; err != nil {
if err := ensureIPForwarding ( cfg . Root , cfg . ProxyTo , cfg . TailnetTargetIP, cfg . Routes) ; err != nil {
log . Printf ( "Failed to enable IP forwarding: %v" , err )
log . Printf ( "Failed to enable IP forwarding: %v" , err )
log . Printf ( "To run tailscale as a proxy or router container, IP forwarding must be enabled." )
log . Printf ( "To run tailscale as a proxy or router container, IP forwarding must be enabled." )
if cfg . InKubernetes {
if cfg . InKubernetes {
@ -270,7 +274,7 @@ authLoop:
}
}
var (
var (
wantProxy = cfg . ProxyTo != ""
wantProxy = cfg . ProxyTo != "" || cfg . TailnetTargetIP != ""
wantDeviceInfo = cfg . InKubernetes && cfg . KubeSecret != "" && cfg . KubernetesCanPatch
wantDeviceInfo = cfg . InKubernetes && cfg . KubeSecret != "" && cfg . KubernetesCanPatch
startupTasksDone = false
startupTasksDone = false
currentIPs deephash . Sum // tailscale IPs assigned to device
currentIPs deephash . Sum // tailscale IPs assigned to device
@ -298,10 +302,12 @@ authLoop:
}
}
if n . NetMap != nil {
if n . NetMap != nil {
addrs := n . NetMap . SelfNode . Addresses ( ) . AsSlice ( )
addrs := n . NetMap . SelfNode . Addresses ( ) . AsSlice ( )
if cfg . ProxyTo != "" && len ( addrs ) > 0 && deephash . Update ( & currentIPs , & addrs ) {
newCurrentIPs := deephash . Hash ( & addrs )
ipsHaveChanged := newCurrentIPs != currentIPs
if cfg . ProxyTo != "" && len ( addrs ) > 0 && ipsHaveChanged {
log . Printf ( "Installing proxy rules" )
log . Printf ( "Installing proxy rules" )
if err := installIPTablesRule ( ctx , cfg . ProxyTo , addrs ) ; err != nil {
if err := installI ngressForwarding Rule( ctx , cfg . ProxyTo , addrs ) ; err != nil {
log . Fatalf ( "installing proxy rules: %v", err )
log . Fatalf ( "installing ingress proxy rules: %v", err )
}
}
}
}
if cfg . ServeConfigPath != "" && len ( n . NetMap . DNS . CertDomains ) > 0 {
if cfg . ServeConfigPath != "" && len ( n . NetMap . DNS . CertDomains ) > 0 {
@ -314,6 +320,13 @@ authLoop:
}
}
}
}
}
}
if cfg . TailnetTargetIP != "" && ipsHaveChanged && len ( addrs ) > 0 {
if err := installEgressForwardingRule ( ctx , cfg . TailnetTargetIP , addrs ) ; err != nil {
log . Fatalf ( "installing egress proxy rules: %v" , err )
}
}
currentIPs = newCurrentIPs
deviceInfo := [ ] any { n . NetMap . SelfNode . StableID ( ) , n . NetMap . SelfNode . Name ( ) }
deviceInfo := [ ] any { n . NetMap . SelfNode . StableID ( ) , n . NetMap . SelfNode . Name ( ) }
if cfg . InKubernetes && cfg . KubernetesCanPatch && cfg . KubeSecret != "" && deephash . Update ( & currentDeviceInfo , & deviceInfo ) {
if cfg . InKubernetes && cfg . KubernetesCanPatch && cfg . KubeSecret != "" && deephash . Update ( & currentDeviceInfo , & deviceInfo ) {
if err := storeDeviceInfo ( ctx , cfg . KubeSecret , n . NetMap . SelfNode . StableID ( ) , n . NetMap . SelfNode . Name ( ) , n . NetMap . SelfNode . Addresses ( ) . AsSlice ( ) ) ; err != nil {
if err := storeDeviceInfo ( ctx , cfg . KubeSecret , n . NetMap . SelfNode . StableID ( ) , n . NetMap . SelfNode . Name ( ) , n . NetMap . SelfNode . Addresses ( ) . AsSlice ( ) ) ; err != nil {
@ -572,14 +585,25 @@ func ensureTunFile(root string) error {
}
}
// ensureIPForwarding enables IPv4/IPv6 forwarding for the container.
// ensureIPForwarding enables IPv4/IPv6 forwarding for the container.
func ensureIPForwarding ( root , proxyTo , routes string ) error {
func ensureIPForwarding ( root , clusterProxyTarget, tailnetTargetiP , routes string ) error {
var (
var (
v4Forwarding , v6Forwarding bool
v4Forwarding , v6Forwarding bool
)
)
if proxyTo != "" {
if clusterProxyTarget != "" {
proxyIP , err := netip . ParseAddr ( proxyTo )
proxyIP , err := netip . ParseAddr ( clusterProxyTarget )
if err != nil {
if err != nil {
return fmt . Errorf ( "invalid proxy destination IP: %v" , err )
return fmt . Errorf ( "invalid cluster destination IP: %v" , err )
}
if proxyIP . Is4 ( ) {
v4Forwarding = true
} else {
v6Forwarding = true
}
}
if tailnetTargetiP != "" {
proxyIP , err := netip . ParseAddr ( tailnetTargetiP )
if err != nil {
return fmt . Errorf ( "invalid tailnet destination IP: %v" , err )
}
}
if proxyIP . Is4 ( ) {
if proxyIP . Is4 ( ) {
v4Forwarding = true
v4Forwarding = true
@ -629,7 +653,53 @@ func ensureIPForwarding(root, proxyTo, routes string) error {
return nil
return nil
}
}
func installIPTablesRule ( ctx context . Context , dstStr string , tsIPs [ ] netip . Prefix ) error {
func installEgressForwardingRule ( ctx context . Context , dstStr string , tsIPs [ ] netip . Prefix ) error {
dst , err := netip . ParseAddr ( dstStr )
if err != nil {
return err
}
argv0 := "iptables"
if dst . Is6 ( ) {
argv0 = "ip6tables"
}
var local string
for _ , pfx := range tsIPs {
if ! pfx . IsSingleIP ( ) {
continue
}
if pfx . Addr ( ) . Is4 ( ) != dst . Is4 ( ) {
continue
}
local = pfx . Addr ( ) . String ( )
break
}
if local == "" {
return fmt . Errorf ( "no tailscale IP matching family of %s found in %v" , dstStr , tsIPs )
}
// Technically, if the control server ever changes the IPs assigned to this
// node, we'll slowly accumulate iptables rules. This shouldn't happen, so
// for now we'll live with it.
// Set up a rule that ensures that all packets
// except for those received on tailscale0 interface is forwarded to
// destination address
cmdDNAT := exec . CommandContext ( ctx , argv0 , "-t" , "nat" , "-I" , "PREROUTING" , "1" , "!" , "-i" , "tailscale0" , "-j" , "DNAT" , "--to-destination" , dstStr )
cmdDNAT . Stdout = os . Stdout
cmdDNAT . Stderr = os . Stderr
if err := cmdDNAT . Run ( ) ; err != nil {
return fmt . Errorf ( "executing iptables failed: %w" , err )
}
// Set up a rule that ensures that all packets sent to the destination
// address will have the proxy's IP set as source IP
cmdSNAT := exec . CommandContext ( ctx , argv0 , "-t" , "nat" , "-I" , "POSTROUTING" , "1" , "--destination" , dstStr , "-j" , "SNAT" , "--to-source" , local )
cmdSNAT . Stdout = os . Stdout
cmdSNAT . Stderr = os . Stderr
if err := cmdSNAT . Run ( ) ; err != nil {
return fmt . Errorf ( "setting up SNAT via iptables failed: %w" , err )
}
return nil
}
func installIngressForwardingRule ( ctx context . Context , dstStr string , tsIPs [ ] netip . Prefix ) error {
dst , err := netip . ParseAddr ( dstStr )
dst , err := netip . ParseAddr ( dstStr )
if err != nil {
if err != nil {
return err
return err
@ -666,10 +736,17 @@ func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Prefi
// settings is all the configuration for containerboot.
// settings is all the configuration for containerboot.
type settings struct {
type settings struct {
AuthKey string
AuthKey string
Hostname string
Hostname string
Routes string
Routes string
ProxyTo string
// ProxyTo is the destination IP to which all incoming
// Tailscale traffic should be proxied. If empty, no proxying
// is done. This is typically a locally reachable IP.
ProxyTo string
// TailnetTargetIP is the destination IP to which all incoming
// non-Tailscale traffic should be proxied. If empty, no
// proxying is done. This is typically a Tailscale IP.
TailnetTargetIP string
ServeConfigPath string
ServeConfigPath string
DaemonExtraArgs string
DaemonExtraArgs string
ExtraArgs string
ExtraArgs string