2023-03-02 22:02:37 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The sniproxy is an outbound SNI proxy. It receives TLS connections over
// Tailscale on one or more TCP ports and sends them out to the same SNI
2023-08-07 14:51:47 +00:00
// hostname & port on the internet. It can optionally forward one or more
// TCP ports to a specific destination. It only does TCP.
2023-03-02 22:02:37 +00:00
package main
import (
"context"
2023-08-07 14:51:47 +00:00
"errors"
2023-03-02 22:02:37 +00:00
"flag"
2023-08-07 14:51:47 +00:00
"fmt"
2023-03-02 22:02:37 +00:00
"log"
"net"
2023-03-07 16:46:02 +00:00
"net/http"
2023-10-20 00:07:07 +00:00
"net/netip"
2023-08-25 14:27:15 +00:00
"os"
2023-10-20 00:07:07 +00:00
"sort"
2023-08-07 14:51:47 +00:00
"strconv"
2023-03-02 22:02:37 +00:00
"strings"
2023-08-25 14:27:15 +00:00
"github.com/peterbourgon/ff/v3"
2023-03-06 00:40:15 +00:00
"golang.org/x/net/dns/dnsmessage"
2023-10-20 00:07:07 +00:00
"tailscale.com/appc"
2023-03-02 22:02:37 +00:00
"tailscale.com/client/tailscale"
2023-04-29 04:28:52 +00:00
"tailscale.com/hostinfo"
2023-10-20 00:07:07 +00:00
"tailscale.com/ipn"
"tailscale.com/tailcfg"
2023-03-02 22:02:37 +00:00
"tailscale.com/tsnet"
2023-08-07 14:51:47 +00:00
"tailscale.com/tsweb"
2023-10-20 00:07:07 +00:00
"tailscale.com/types/appctype"
"tailscale.com/types/ipproto"
2023-03-05 20:13:36 +00:00
"tailscale.com/types/nettype"
2023-10-20 00:07:07 +00:00
"tailscale.com/util/mak"
2023-03-02 22:02:37 +00:00
)
2023-10-20 00:07:07 +00:00
const configCapKey = "tailscale.com/sniproxy"
2023-03-06 00:40:15 +00:00
var tsMBox = dnsmessage . MustNewName ( "support.tailscale.com." )
2023-08-07 14:51:47 +00:00
// portForward is the state for a single port forwarding entry, as passed to the --forward flag.
type portForward struct {
Port int
Proto string
Destination string
}
// parseForward takes a proto/port/destination tuple as an input, as would be passed
// to the --forward command line flag, and returns a *portForward struct of those parameters.
func parseForward ( value string ) ( * portForward , error ) {
parts := strings . Split ( value , "/" )
if len ( parts ) != 3 {
return nil , errors . New ( "cannot parse: " + value )
}
proto := parts [ 0 ]
if proto != "tcp" {
return nil , errors . New ( "unsupported forwarding protocol: " + proto )
}
port , err := strconv . ParseUint ( parts [ 1 ] , 10 , 16 )
if err != nil {
return nil , errors . New ( "bad forwarding port: " + parts [ 1 ] )
}
host := parts [ 2 ]
if host == "" {
return nil , errors . New ( "bad destination: " + value )
}
return & portForward { Port : int ( port ) , Proto : proto , Destination : host } , nil
}
2023-07-29 07:11:19 +00:00
2023-03-02 22:02:37 +00:00
func main ( ) {
2023-10-20 00:07:07 +00:00
// Parse flags
2023-08-25 14:27:15 +00:00
fs := flag . NewFlagSet ( "sniproxy" , flag . ContinueOnError )
var (
ports = fs . String ( "ports" , "443" , "comma-separated list of ports to proxy" )
forwards = fs . String ( "forwards" , "" , "comma-separated list of ports to transparently forward, protocol/number/destination. For example, --forwards=tcp/22/github.com,tcp/5432/sql.example.com" )
wgPort = fs . Int ( "wg-listen-port" , 0 , "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select" )
promoteHTTPS = fs . Bool ( "promote-https" , true , "promote HTTP to HTTPS" )
2023-08-30 15:18:49 +00:00
debugPort = fs . Int ( "debug-port" , 8893 , "Listening port for debug/metrics endpoint" )
2023-10-08 17:26:48 +00:00
hostname = fs . String ( "hostname" , "" , "Hostname to register the service under" )
2023-08-25 14:27:15 +00:00
)
err := ff . Parse ( fs , os . Args [ 1 : ] , ff . WithEnvVarPrefix ( "TS_APPC" ) )
if err != nil {
log . Fatal ( "ff.Parse" )
}
2023-03-02 22:02:37 +00:00
2023-10-20 00:07:07 +00:00
var ts tsnet . Server
defer ts . Close ( )
2023-04-29 04:28:52 +00:00
2023-10-20 00:07:07 +00:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
run ( ctx , & ts , * wgPort , * hostname , * promoteHTTPS , * debugPort , * ports , * forwards )
}
// run actually runs the sniproxy. Its separate from main() to assist in testing.
func run ( ctx context . Context , ts * tsnet . Server , wgPort int , hostname string , promoteHTTPS bool , debugPort int , ports , forwards string ) {
// Wire up Tailscale node + app connector server
hostinfo . SetApp ( "sniproxy" )
2023-03-02 22:02:37 +00:00
var s server
2023-10-20 00:07:07 +00:00
s . ts = ts
s . ts . Port = uint16 ( wgPort )
s . ts . Hostname = hostname
2023-03-02 22:02:37 +00:00
lc , err := s . ts . LocalClient ( )
if err != nil {
2023-10-20 00:07:07 +00:00
log . Fatalf ( "LocalClient() failed: %v" , err )
2023-03-02 22:02:37 +00:00
}
s . lc = lc
2023-10-20 00:07:07 +00:00
s . ts . RegisterFallbackTCPHandler ( s . appc . HandleTCPFlow )
2023-08-07 14:51:47 +00:00
2023-10-20 00:07:07 +00:00
// Start special-purpose listeners: dns, http promotion, debug server
2023-03-05 20:13:36 +00:00
ln , err := s . ts . Listen ( "udp" , ":53" )
if err != nil {
2023-10-20 00:07:07 +00:00
log . Fatalf ( "failed listening on port 53: %v" , err )
2023-03-05 20:13:36 +00:00
}
2023-10-20 00:07:07 +00:00
defer ln . Close ( )
2023-03-05 20:13:36 +00:00
go s . serveDNS ( ln )
2023-10-20 00:07:07 +00:00
if promoteHTTPS {
2023-03-07 16:46:02 +00:00
ln , err := s . ts . Listen ( "tcp" , ":80" )
if err != nil {
2023-10-20 00:07:07 +00:00
log . Fatalf ( "failed listening on port 80: %v" , err )
2023-03-07 16:46:02 +00:00
}
2023-10-20 00:07:07 +00:00
defer ln . Close ( )
2023-03-07 16:46:02 +00:00
log . Printf ( "Promoting HTTP to HTTPS ..." )
go s . promoteHTTPS ( ln )
}
2023-10-20 00:07:07 +00:00
if debugPort != 0 {
2023-08-07 14:51:47 +00:00
mux := http . NewServeMux ( )
tsweb . Debugger ( mux )
2023-10-20 00:07:07 +00:00
dln , err := s . ts . Listen ( "tcp" , fmt . Sprintf ( ":%d" , debugPort ) )
2023-08-07 14:51:47 +00:00
if err != nil {
2023-10-20 00:07:07 +00:00
log . Fatalf ( "failed listening on debug port: %v" , err )
2023-08-07 14:51:47 +00:00
}
2023-10-20 00:07:07 +00:00
defer dln . Close ( )
2023-08-07 14:51:47 +00:00
go func ( ) {
2023-10-20 00:07:07 +00:00
log . Fatalf ( "debug serve: %v" , http . Serve ( dln , mux ) )
2023-08-07 14:51:47 +00:00
} ( )
}
2023-10-20 00:07:07 +00:00
// Finally, start mainloop to configure app connector based on information
// in the netmap.
// We set the NotifyInitialNetMap flag so we will always get woken with the
// current netmap, before only being woken on changes.
bus , err := lc . WatchIPNBus ( ctx , ipn . NotifyWatchEngineUpdates | ipn . NotifyInitialNetMap | ipn . NotifyNoPrivateKeys )
if err != nil {
log . Fatalf ( "watching IPN bus: %v" , err )
2023-03-02 22:02:37 +00:00
}
2023-10-20 00:07:07 +00:00
defer bus . Close ( )
2023-08-07 14:51:47 +00:00
for {
2023-10-20 00:07:07 +00:00
msg , err := bus . Next ( )
2023-08-07 14:51:47 +00:00
if err != nil {
2023-10-20 00:07:07 +00:00
if errors . Is ( err , context . Canceled ) {
return
}
log . Fatalf ( "reading IPN bus: %v" , err )
2023-08-07 14:51:47 +00:00
}
2023-10-20 00:07:07 +00:00
// NetMap contains app-connector configuration
if nm := msg . NetMap ; nm != nil && nm . SelfNode . Valid ( ) {
sn := nm . SelfNode . AsStruct ( )
var c appctype . AppConnectorConfig
nmConf , err := tailcfg . UnmarshalNodeCapJSON [ appctype . AppConnectorConfig ] ( sn . CapMap , configCapKey )
if err != nil {
log . Printf ( "failed to read app connector configuration from coordination server: %v" , err )
} else if len ( nmConf ) > 0 {
c = nmConf [ 0 ]
}
if c . AdvertiseRoutes {
if err := s . advertiseRoutesFromConfig ( ctx , & c ) ; err != nil {
log . Printf ( "failed to advertise routes: %v" , err )
}
}
// Backwards compatibility: combine any configuration from control with flags specified
// on the command line. This is intentionally done after we advertise any routes
// because its never correct to advertise the nodes native IP addresses.
s . mergeConfigFromFlags ( & c , ports , forwards )
s . appc . Configure ( & c )
2023-03-05 20:13:36 +00:00
}
}
}
2023-10-20 00:07:07 +00:00
type server struct {
appc appc . Server
ts * tsnet . Server
lc * tailscale . LocalClient
2023-03-05 20:13:36 +00:00
}
2023-10-20 00:07:07 +00:00
func ( s * server ) advertiseRoutesFromConfig ( ctx context . Context , c * appctype . AppConnectorConfig ) error {
// Collect the set of addresses to advertise, using a map
// to avoid duplicate entries.
addrs := map [ netip . Addr ] struct { } { }
for _ , c := range c . SNIProxy {
for _ , ip := range c . Addrs {
addrs [ ip ] = struct { } { }
}
}
for _ , c := range c . DNAT {
for _ , ip := range c . Addrs {
addrs [ ip ] = struct { } { }
}
2023-03-02 22:02:37 +00:00
}
2023-10-20 00:07:07 +00:00
var routes [ ] netip . Prefix
for a := range addrs {
routes = append ( routes , netip . PrefixFrom ( a , a . BitLen ( ) ) )
2023-03-02 22:02:37 +00:00
}
2023-10-20 00:07:07 +00:00
sort . SliceStable ( routes , func ( i , j int ) bool {
return routes [ i ] . Addr ( ) . Less ( routes [ j ] . Addr ( ) ) // determinism r us
2023-03-02 22:02:37 +00:00
} )
2023-03-06 00:40:15 +00:00
2023-10-20 00:07:07 +00:00
_ , err := s . lc . EditPrefs ( ctx , & ipn . MaskedPrefs {
Prefs : ipn . Prefs {
AdvertiseRoutes : routes ,
} ,
AdvertiseRoutesSet : true ,
} )
return err
2023-08-07 14:51:47 +00:00
}
2023-10-20 00:07:07 +00:00
func ( s * server ) mergeConfigFromFlags ( out * appctype . AppConnectorConfig , ports , forwards string ) {
ip4 , ip6 := s . ts . TailscaleIPs ( )
2023-08-07 14:51:47 +00:00
2023-10-20 00:07:07 +00:00
sniConfigFromFlags := appctype . SNIProxyConfig {
Addrs : [ ] netip . Addr { ip4 , ip6 } ,
2023-08-07 14:51:47 +00:00
}
2023-10-20 00:07:07 +00:00
if ports != "" {
for _ , portStr := range strings . Split ( ports , "," ) {
port , err := strconv . ParseUint ( portStr , 10 , 16 )
if err != nil {
log . Fatalf ( "invalid port: %s" , portStr )
}
sniConfigFromFlags . IP = append ( sniConfigFromFlags . IP , tailcfg . ProtoPortRange {
Proto : int ( ipproto . TCP ) ,
Ports : tailcfg . PortRange { First : uint16 ( port ) , Last : uint16 ( port ) } ,
} )
}
2023-08-07 14:51:47 +00:00
}
2023-10-20 00:07:07 +00:00
var forwardConfigFromFlags [ ] appctype . DNATConfig
for _ , forwStr := range strings . Split ( forwards , "," ) {
if forwStr == "" {
continue
}
forw , err := parseForward ( forwStr )
if err != nil {
log . Printf ( "invalid forwarding spec: %v" , err )
continue
}
2023-08-07 14:51:47 +00:00
2023-10-20 00:07:07 +00:00
forwardConfigFromFlags = append ( forwardConfigFromFlags , appctype . DNATConfig {
Addrs : [ ] netip . Addr { ip4 , ip6 } ,
To : [ ] string { forw . Destination } ,
IP : [ ] tailcfg . ProtoPortRange {
{
Proto : int ( ipproto . TCP ) ,
Ports : tailcfg . PortRange { First : uint16 ( forw . Port ) , Last : uint16 ( forw . Port ) } ,
} ,
} ,
2023-03-06 00:40:15 +00:00
} )
}
2023-10-20 00:07:07 +00:00
if len ( forwardConfigFromFlags ) == 0 && len ( sniConfigFromFlags . IP ) == 0 {
return // no config specified on the command line
2023-03-06 00:40:15 +00:00
}
2023-10-20 00:07:07 +00:00
mak . Set ( & out . SNIProxy , "flags" , sniConfigFromFlags )
for i , forward := range forwardConfigFromFlags {
mak . Set ( & out . DNAT , appctype . ConfigID ( fmt . Sprintf ( "flags_%d" , i ) ) , forward )
2023-03-06 00:40:15 +00:00
}
2023-10-20 00:07:07 +00:00
}
2023-03-06 00:40:15 +00:00
2023-10-20 00:07:07 +00:00
func ( s * server ) serveDNS ( ln net . Listener ) {
for {
c , err := ln . Accept ( )
if err != nil {
log . Printf ( "serveDNS accept: %v" , err )
return
}
go s . appc . HandleDNS ( c . ( nettype . ConnPacketConn ) )
2023-03-06 00:40:15 +00:00
}
}
2023-03-07 16:46:02 +00:00
func ( s * server ) promoteHTTPS ( ln net . Listener ) {
err := http . Serve ( ln , http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
http . Redirect ( w , r , "https://" + r . Host + r . RequestURI , http . StatusFound )
} ) )
log . Fatalf ( "promoteHTTPS http.Serve: %v" , err )
}