Skip to content

Commit

Permalink
feat: tailscale socks5/http proxy support
Browse files Browse the repository at this point in the history
  • Loading branch information
Menci committed Aug 27, 2024
1 parent becc7fd commit 8d12718
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 41 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ go.work.sum

# built binary
/tsukasa

# debug
/config.yaml
/state
28 changes: 28 additions & 0 deletions LICENSE.Tailscale
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
BSD 3-Clause License

Copyright (c) 2020 Tailscale Inc & AUTHORS.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Tsusaka is a flexible port forwarder among:
* TCP Ports
* UNIX Sockets
* Tailscale TCP Ports (without Tailscale daemon or TUN/TAP permission! This is made possible with [tsnet](https://tailscale.com/kb/1244/tsnet))
* Also supports SOCKS5/HTTP proxy feature of `tailscaled`.
* If you don't use Tailscale features, it won't initialize Tailscale components and just behaves like a local port forwarder.

It also supports passing the client IP with PROXY protocol (for listening on TCP or Tailscale TCP).
Expand All @@ -24,6 +25,8 @@ Use with command-line configuration:
--ts-authkey "$TS_AUTHKEY" \
--ts-ephemeral false \
--ts-state-dir /var/lib/tailscale \
--ts-listen-socks5 localhost:1080 \
--ts-listen-http localhost:8080 \
--ts-verbose true \
nginx,listen=tailscale://0.0.0.0:80,connect=tcp://127.0.0.1:8080,log-level=info,proxy-protocol \
myapp,listen=unix:/var/run/myapp.sock,connect=tailscale://app-hosted-in-tailnet:8080
Expand All @@ -39,6 +42,9 @@ tailscale:
authKey: null
ephemeral: false
stateDir: /var/lib/tailscale
listen:
socks5: 1080
http: 8080
verbose: true
services:
nginx:
Expand Down
20 changes: 20 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Tailscale configuration is not required and Tailsccale will not be loaded if no services with Tailscale defined.
tailscale:
hostname: Tsusaka
# `null` to Use `TS_AUTHKEY` from environment or interactive login.
authKey: null
ephemeral: false
stateDir: /var/lib/tailscale
listen:
socks5: 1080
http: 8080
verbose: true
services:
nginx:
listen: tailscale://0.0.0.0:80 # Only "0.0.0.0" and "::" allowed in Tailscale listener.
connect: tcp://127.0.0.1:8080
logLevel: info # "error" / "info" / "verbose". By default "info".
proxyProtocol: true # Listening on UNIX socket doesn't support PROXY protocol.
myapp:
listen: unix:/var/run/myapp.sock
connect: tailscale://app-hosted-in-tailnet:8080
103 changes: 70 additions & 33 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ import (
"fmt"
"os"
"regexp"
"strconv"
"strings"

"gopkg.in/yaml.v2"
)

type TailscaleListenConfig struct {
Socks5 string `yaml:"socks5,omitempty"`
HTTP string `yaml:"http,omitempty"`
}

type TailscaleConfig struct {
Hostname string `yaml:"hostname,omitempty"`
AuthKey string `yaml:"authKey"`
Ephemeral bool `yaml:"ephemeral,omitempty"`
StateDir string `yaml:"stateDir"`
Verbose bool `yaml:"verbose,omitempty"`
Hostname string `yaml:"hostname,omitempty"`
AuthKey string `yaml:"authKey"`
Ephemeral bool `yaml:"ephemeral,omitempty"`
StateDir string `yaml:"stateDir"`
Listen TailscaleListenConfig `yaml:"listen,omitempty"`
Verbose bool `yaml:"verbose,omitempty"`
}

type ServiceConfig struct {
Expand Down Expand Up @@ -47,27 +54,48 @@ type Config struct {
Services map[string]*ServiceConfig `yaml:"services"`
}

// A private struct to hold the command-line flags
type boolFlag struct {
value bool
set bool
}

func (f *boolFlag) Set(value string) error {
v, err := strconv.ParseBool(value)
if err != nil {
return err
}
f.value = v
f.set = true
return nil
}

func (f *boolFlag) String() string {
return strconv.FormatBool(f.value)
}

type arguments struct {
conf *string
tsHostname *string
tsAuthKey *string
tsEphemeral *bool
tsStateDir *string
tsVerbose *bool
conf string
tsHostname string
tsAuthKey string
tsEphemeral boolFlag
tsStateDir string
tsListenSocks5 string
tsListenHttp string
tsVerbose boolFlag

services []string
}

func parseArguments() *arguments {
flags := &arguments{
conf: flag.String("conf", "", "YAML Configuration file"),
tsHostname: flag.String("ts-hostname", "", "Tailscale hostname"),
tsAuthKey: flag.String("ts-authkey", "", "Tailscale authentication key (default to $TS_AUTHKEY)"),
tsEphemeral: flag.Bool("ts-ephemeral", false, "Set the Tailscale host to ephemeral"),
tsStateDir: flag.String("ts-state-dir", "", "Tailscale state directory"),
tsVerbose: flag.Bool("ts-verbose", false, "Print Tailscale logs"),
}
flags := &arguments{}
flag.StringVar(&flags.conf, "conf", "", "YAML Configuration file")
flag.StringVar(&flags.tsHostname, "ts-hostname", "", "Tailscale hostname")
flag.StringVar(&flags.tsAuthKey, "ts-authkey", "", "Tailscale authentication key (default to $TS_AUTHKEY)")
flag.Var(&flags.tsEphemeral, "ts-ephemeral", "Set the Tailscale host to ephemeral")
flag.StringVar(&flags.tsStateDir, "ts-state-dir", "", "Tailscale state directory")
flag.StringVar(&flags.tsListenSocks5, "ts-listen-socks5", "", "Start SOCKS5 proxy server on [host]:port to access Tailnet")
flag.StringVar(&flags.tsListenHttp, "ts-listen-http", "", "Start HTTP proxy server on [host]:port to access Tailnet")
flag.Var(&flags.tsVerbose, "ts-verbose", "Print Tailscale logs")
flag.Usage = func() {
f := flag.CommandLine.Output()
fmt.Fprintf(f, "Usage: %s [options] service1 service2 ...\n", os.Args[0])
Expand All @@ -78,10 +106,11 @@ func parseArguments() *arguments {
fmt.Fprintln(f, " --ts-authkey \"$TS_AUTHKEY\" \\")
fmt.Fprintln(f, " --ts-ephemeral false \\")
fmt.Fprintln(f, " --ts-state-dir /var/lib/tailscale \\")
fmt.Fprintln(f, " --ts-listen-socks5 127.0.0.1:1118 \\")
fmt.Fprintln(f, " --ts-listen-http 127.0.0.1:8080 \\")
fmt.Fprintln(f, " --ts-verbose true \\")
fmt.Fprintln(f, " nginx,listen=tailscale://0.0.0.0:80,connect=tcp://127.0.0.1:8080,log-level=info,proxy-protocol \\")
fmt.Fprintln(f, " myapp,listen=unix:/var/run/myapp.sock,connect=tailscale://app-hosted-in-tailnet:8080")

}
flag.Parse()
flags.services = flag.Args()
Expand Down Expand Up @@ -147,24 +176,32 @@ func parseService(s string) (name string, service *ServiceConfig, err error) {
}

func mergeConfig(c *Config, a *arguments) error {
if a.tsHostname != nil && *a.tsHostname != "" {
c.Tailscale.Hostname = *a.tsHostname
if a.tsHostname != "" {
c.Tailscale.Hostname = a.tsHostname
}

if a.tsAuthKey != "" {
c.Tailscale.AuthKey = a.tsAuthKey
}

if a.tsEphemeral.set {
c.Tailscale.Ephemeral = a.tsEphemeral.value
}

if a.tsAuthKey != nil && *a.tsAuthKey != "" {
c.Tailscale.AuthKey = *a.tsAuthKey
if a.tsStateDir != "" {
c.Tailscale.StateDir = a.tsStateDir
}

if a.tsEphemeral != nil {
c.Tailscale.Ephemeral = *a.tsEphemeral
if a.tsListenSocks5 != "" {
c.Tailscale.Listen.Socks5 = a.tsListenSocks5
}

if a.tsStateDir != nil && *a.tsStateDir != "" {
c.Tailscale.StateDir = *a.tsStateDir
if a.tsListenHttp != "" {
c.Tailscale.Listen.HTTP = a.tsListenHttp
}

if a.tsVerbose != nil {
c.Tailscale.Verbose = *a.tsVerbose
if a.tsVerbose.set {
c.Tailscale.Verbose = a.tsVerbose.value
}

for _, s := range a.services {
Expand Down Expand Up @@ -211,8 +248,8 @@ func GetConfig() (*Config, error) {
Tailscale: TailscaleConfig{},
Services: make(map[string]*ServiceConfig),
}
if *a.conf != "" {
f, err := os.Open(*a.conf)
if a.conf != "" {
f, err := os.Open(a.conf)
if err != nil {
return nil, err
}
Expand Down
36 changes: 28 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"log"
"os"
"os/signal"
"sync"
Expand All @@ -13,7 +12,7 @@ import (
type Logf func(format string, args ...any)

func main() {
logger := CreateLogger("tsukasa", Info)
logger := CreateLogger("main", Info)

config, err := GetConfig()
if err != nil {
Expand All @@ -29,12 +28,16 @@ func main() {
tsnet.AuthKey = config.Tailscale.AuthKey
tsnet.Ephemeral = config.Tailscale.Ephemeral
tsnet.Dir = config.Tailscale.StateDir
tsnet.UserLogf = log.New(os.Stderr, "[tsnet] ", log.LstdFlags).Printf

var tsLogLevel LogLevel
if config.Tailscale.Verbose {
tsnet.Logf = tsnet.UserLogf
tsLogLevel = Verbose
} else {
tsnet.Logf = func(format string, args ...any) {}
tsLogLevel = Info
}
tsLogger := CreateLogger("tailscale", tsLogLevel)
tsnet.Logf = tsLogger.Verbosef
tsnet.UserLogf = tsLogger.Infof

shutdownCh := make(chan struct{})
shutdownWg := &sync.WaitGroup{}
Expand All @@ -45,6 +48,10 @@ func main() {
}

usingTailscale := false
if config.Tailscale.Listen.Socks5 != "" || config.Tailscale.Listen.HTTP != "" {
usingTailscale = true
}

var services []*Service
for name, serviceConfig := range config.Services {
service, err := CreateService(serviceContext, name, serviceConfig)
Expand All @@ -70,14 +77,27 @@ func main() {
defer tsnet.Close()
}

// Start services.
if len(services) == 0 {
logger.Fatalf("no services defined. run %s -h for help", os.Args[0])
somethingRunning := false

if config.Tailscale.Listen.Socks5 != "" {
somethingRunning = true
StartProxy(tsLogger, config.Tailscale.Listen.Socks5, tsnet.Dial, Socks5)
}
if config.Tailscale.Listen.HTTP != "" {
somethingRunning = true
StartProxy(tsLogger, config.Tailscale.Listen.HTTP, tsnet.Dial, HTTP)
}

// Start services.
for _, service := range services {
somethingRunning = true
go service.Start()
}

if !somethingRunning {
logger.Fatalf("no listener defined. run %s -h for help", os.Args[0])
}

// Wait for signal to shutdown.
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
Expand Down
Loading

0 comments on commit 8d12718

Please sign in to comment.