From a249cc60f89814389844563c2183f664225d057d Mon Sep 17 00:00:00 2001 From: Menci Date: Wed, 21 Aug 2024 23:09:45 +0800 Subject: [PATCH] feat: initial commit --- .github/workflows/docker-image.yaml | 31 ++++ .gitignore | 28 ++++ Dockerfile | 10 ++ LICENSE | 21 +++ README.md | 109 +++++++++++++ config.go | 242 ++++++++++++++++++++++++++++ go.mod | 89 ++++++++++ go.sum | 181 +++++++++++++++++++++ logger.go | 50 ++++++ main.go | 87 ++++++++++ pipe.go | 34 ++++ service.go | 240 +++++++++++++++++++++++++++ 12 files changed, 1122 insertions(+) create mode 100644 .github/workflows/docker-image.yaml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logger.go create mode 100644 main.go create mode 100644 pipe.go create mode 100644 service.go diff --git a/.github/workflows/docker-image.yaml b/.github/workflows/docker-image.yaml new file mode 100644 index 0000000..c8e7b3b --- /dev/null +++ b/.github/workflows/docker-image.yaml @@ -0,0 +1,31 @@ +name: Docker Image + +on: + workflow_dispatch: + push: + +jobs: + build_push: + name: Build and Push + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build Container Image + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/riscv64 + push: true + tags: ghcr.io/menci/tsukasa:latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f2c696 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# built binary +/tsukasa diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..377c11f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +ARG ALPINE_VERSION=3.20 +ARG GOLANG_VERSION=1.22.5 + +FROM golang:${GOLANG_VERSION}-alpine AS builder +COPY . /build +RUN cd /build && go build -o tsukasa + +FROM alpine:$ALPINE_VERSION +COPY --from=builder /build/tsukasa /usr/local/bin/tsukasa +ENTRYPOINT ["/usr/local/bin/tsukasa"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e4716c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Menci + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fdd07cf --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Tsusaka + +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)) + * 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). + +> The name Tsusaka comes from the character **Tenma Tsukasa** from the music visual novel game Project SEKAI. He is a member of the musical show unit "Wonderlands x Showtime". Tsukasa has bucketloads of confidence and loves to be the center of attention. A theater show he saw as a kid impressed him so much that he made it his ultimate goal to become the greatest star in the world. + +# Development + +Simply build the program with `go build` or build the Docker image with `docker build`. + +# Usage + +Use with command-line configuration: + +```bash +./tsusaka --ts-hostname Tsusaka \ + --ts-authkey "$TS_AUTHKEY" \ + --ts-ephemeral false \ + --ts-state-dir /var/lib/tailscale \ + --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 +``` + +Or use with configuration file: + +```yaml +# 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 + 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 +``` + +Configuration file could be specified with command-line configuration options at the same time. + +```bash +./tsusaka --conf tsusaka.yaml +``` + +You can also completely omit Tailscale-related configuration and use Tsukasa as a simple port forward between TCP port and UNIX socket. + +# Docker + +To use Tsukasa with Docker, it's recommended to start Tsusaka in the host network mode to ensure Tailscale's UDP hole punching to work (Docker's MASQUERADE routing is nearly blocking NAT traversal). + +```bash +docker run \ + --network=host \ + -e TS_AUTHKEY="$TS_AUTHKEY" \ + -v ./tailscale-state:/var/lib/tailscale \ + ghcr.io/menci/tsusaka \ + --ts-hostname Tsusaka \ + --ts-state-dir /var/lib/tailscale \ + myapp,listen=tcp://0.0.0.0:80,connect=tailscale://app-hosted-in-tailnet:8080 +``` + +If you want to expose something in a container to your Tailnet, use UNIX socket and a shared volume. Here is an example with [Docker Compose](https://docs.docker.com/compose/). Note that if your application doesn't support listening on a UNIX socket, you can also start another instance of Tsukasa to work as a simple port forwarder from/to UNIX socket and TCP port in the virtual network. + +```yaml +services: + initialize: + image: busybox + command: | + # The initialize container empties the shared-sockets directory each time. + rm -rf /socket/* + volumes: + - shared-sockets:/socket + tsukasa: + image: ghcr.io/menci/tsukasa + network_mode: host + depends_on: + initialize: + condition: service_completed_successfully + volumes: + - tailscale-state:/var/lib/tailscale + - shared-sockets:/socket + environment: + TS_AUTHKEY: ${TAILSCALE_AUTHKEY} + command: + - app,listen=tailscale://0.0.0.0:80,connect=unix:/socket/app.sock + app: + image: # Here comes your app, which listens on /socket/app.sock + depends_on: + initialize: + condition: service_completed_successfully + volumes: + - shared-sockets:/socket + command: my_app --listen /socket/app.sock +``` diff --git a/config.go b/config.go new file mode 100644 index 0000000..f70bab1 --- /dev/null +++ b/config.go @@ -0,0 +1,242 @@ +package main + +import ( + "flag" + "fmt" + "os" + "regexp" + "strings" + + "gopkg.in/yaml.v2" +) + +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"` +} + +type ServiceConfig struct { + Listen string `yaml:"listen"` + Connect string `yaml:"connect"` + logLevel string `yaml:"logLevel,omitempty"` + ProxyProtocol bool `yaml:"proxyProtocol,omitempty"` + + LogLevel LogLevel +} + +func parseLogLevel(s string) (LogLevel, error) { + switch s { + case "error": + return Error, nil + case "info": + return Info, nil + case "verbose": + return Verbose, nil + case "": + return Info, nil + default: + return 0, fmt.Errorf("unknown log level: %s", s) + } +} + +type Config struct { + Tailscale TailscaleConfig `yaml:"tailscale"` + Services map[string]*ServiceConfig `yaml:"services"` +} + +// A private struct to hold the command-line flags +type arguments struct { + conf *string + tsHostname *string + tsAuthKey *string + tsEphemeral *bool + tsStateDir *string + tsVerbose *bool + + 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"), + } + flag.Usage = func() { + f := flag.CommandLine.Output() + fmt.Fprintf(f, "Usage: %s [options] service1 service2 ...\n", os.Args[0]) + fmt.Fprint(f, "\nTsukasa - A flexible port forwarder among TCP, UNIX Socket and Tailscale TCP ports.\n\n") + flag.PrintDefaults() + fmt.Fprintf(f, "\nExample: %s \\\n", os.Args[0]) + fmt.Fprintln(f, " --ts-hostname Tsukasa \\") + 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-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() + return flags +} + +var nameRegexp = regexp.MustCompile(`^[$a-zA-Z0-9_-]+$`) + +func parseService(s string) (name string, service *ServiceConfig, err error) { + // Examples: + // 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 + + // Split the string by commas + parts := strings.Split(s, ",") + + // The first part is the service name + name = parts[0] + parts = parts[1:] + + if !nameRegexp.MatchString(name) { + return "", nil, fmt.Errorf("invalid service name: %s", name) + } + + // The rest of the parts are key-value pairs + service = &ServiceConfig{} + for _, part := range parts { + kv := strings.SplitN(part, "=", 2) + + key := kv[0] + var value *string + if len(kv) == 2 { + value = &kv[1] + } + + switch key { + case "listen": + if value == nil { + return "", nil, fmt.Errorf("required value for option `listen`") + } + service.Listen = *value + case "connect": + if value == nil { + return "", nil, fmt.Errorf("required value for option `connect`") + } + service.Connect = *value + case "log-level": + if value == nil { + return "", nil, fmt.Errorf("required value for option `log-level`") + } + service.logLevel = *value + case "proxy-protocol": + if value != nil { + return "", nil, fmt.Errorf("no value expected for option `proxy-protocol`") + } + service.ProxyProtocol = true + default: + return "", nil, fmt.Errorf("unknown service argument: %s", key) + } + } + + return name, service, nil +} + +func mergeConfig(c *Config, a *arguments) error { + if a.tsHostname != nil && *a.tsHostname != "" { + c.Tailscale.Hostname = *a.tsHostname + } + + if a.tsAuthKey != nil && *a.tsAuthKey != "" { + c.Tailscale.AuthKey = *a.tsAuthKey + } + + if a.tsEphemeral != nil { + c.Tailscale.Ephemeral = *a.tsEphemeral + } + + if a.tsStateDir != nil && *a.tsStateDir != "" { + c.Tailscale.StateDir = *a.tsStateDir + } + + if a.tsVerbose != nil { + c.Tailscale.Verbose = *a.tsVerbose + } + + for _, s := range a.services { + name, service, err := parseService(s) + if err != nil { + return err + } + c.Services[name] = service + } + + return nil +} + +func (c *Config) ValidateTailscaleConfig() error { + if c.Tailscale.Hostname == "" { + return fmt.Errorf("missing Tailscale hostname") + } + + if c.Tailscale.StateDir == "" { + return fmt.Errorf("missing Tailscale state directory") + } + + return nil +} + +func (c *Config) ValidateServices() error { + for name, service := range c.Services { + if service.Listen == "" { + return fmt.Errorf("missing listen address for service %s", name) + } + + if service.Connect == "" { + return fmt.Errorf("missing connect address for service %s", name) + } + } + + return nil +} + +func GetConfig() (*Config, error) { + a := parseArguments() + + c := &Config{ + Tailscale: TailscaleConfig{}, + Services: make(map[string]*ServiceConfig), + } + if *a.conf != "" { + f, err := os.Open(*a.conf) + if err != nil { + return nil, err + } + + err = yaml.NewDecoder(f).Decode(&c) + if err != nil { + return nil, err + } + } + + if err := mergeConfig(c, a); err != nil { + return nil, err + } + + if c.Tailscale.AuthKey == "" { + c.Tailscale.AuthKey = os.Getenv("TS_AUTHKEY") + } + + for name, service := range c.Services { + var err error + if service.LogLevel, err = parseLogLevel(service.logLevel); err != nil { + return nil, fmt.Errorf("invalid log level for service %s: %v", name, err) + } + } + + return c, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..835c9c2 --- /dev/null +++ b/go.mod @@ -0,0 +1,89 @@ +module github.com/Menci/tsukasa + +go 1.22.5 + +require gopkg.in/yaml.v2 v2.4.0 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/akutz/memconn v0.1.0 // indirect + github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect + github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.26.5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect + github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect + github.com/aws/smithy-go v1.19.0 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect + github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect + github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect + github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect + github.com/fxamacker/cbor/v2 v2.6.0 // indirect + github.com/gaissmai/bart v0.11.1 // indirect + github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/csrf v1.7.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/illarion/gonotify v1.0.1 // indirect + github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect + github.com/jsimonetti/rtnetlink v1.4.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect + github.com/mdlayher/genetlink v1.3.2 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/sdnotify v1.0.0 // indirect + github.com/mdlayher/socket v0.5.0 // indirect + github.com/miekg/dns v1.1.58 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/prometheus-community/pro-bing v0.4.0 // indirect + github.com/safchain/ethtool v0.3.0 // indirect + github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect + github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect + github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect + github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect + github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect + github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect + github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect + github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 // indirect + github.com/tcnksm/go-httpstat v0.2.0 // indirect + github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect + github.com/vishvananda/netlink v1.2.1-beta.2 // indirect + github.com/vishvananda/netns v0.0.4 // indirect + github.com/x448/float16 v0.8.4 // indirect + go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.22.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + golang.zx2c4.com/wireguard/windows v0.5.3 // indirect + gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 // indirect + nhooyr.io/websocket v1.8.10 // indirect + tailscale.com v1.70.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6ccfade --- /dev/null +++ b/go.sum @@ -0,0 +1,181 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= +github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= +github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw= +github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU= +github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8= +github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= +github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE= +github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= +github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= +github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= +github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI= +github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= +github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= +github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= +github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= +github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= +github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= +github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= +github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= +github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= +github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= +github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= +github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw= +github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= +github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= +github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= +github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk= +github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= +github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w= +github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU= +github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g= +github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 h1:ycpNCSYwzZ7x4G4ioPNtKQmIY0G/3o4pVf8wCZq6blY= +github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= +github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= +github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw= +github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= +github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8= +go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= +golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM= +gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0= +nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= +nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= +tailscale.com v1.70.0 h1:SW7mxDepkXBv2iKITeyFDEfHCJBfOeHM+U79lQ0d5zQ= +tailscale.com v1.70.0/go.mod h1:a5yWox+uO5CI4tCB9ot0ZPMdQMiC+Pis9mudVaYETIo= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..ff22a80 --- /dev/null +++ b/logger.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "log" + "os" +) + +type LogLevel int + +const ( + Error LogLevel = 0 + Info LogLevel = 1 + Verbose LogLevel = 2 +) + +type Logger struct { + Printf func(format string, v ...interface{}) + LogLevel LogLevel +} + +func (l *Logger) Fatalf(format string, v ...interface{}) { + l.Printf(format, v...) + os.Exit(1) +} + +func (l *Logger) Logf(logLevel LogLevel, format string, v ...interface{}) { + if l.LogLevel >= logLevel { + l.Printf(format, v...) + } +} + +func (l *Logger) Errorf(format string, v ...interface{}) { + l.Logf(Error, format, v...) +} + +func (l *Logger) Infof(format string, v ...interface{}) { + l.Logf(Info, format, v...) +} + +func (l *Logger) Verbosef(format string, v ...interface{}) { + l.Logf(Verbose, format, v...) +} + +func CreateLogger(prefix string, logLevel LogLevel) *Logger { + return &Logger{ + Printf: log.New(os.Stderr, fmt.Sprintf("[%s] ", prefix), log.LstdFlags).Printf, + LogLevel: logLevel, + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..0d43df0 --- /dev/null +++ b/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "log" + "os" + "os/signal" + "sync" + "syscall" + + "tailscale.com/tsnet" +) + +type Logf func(format string, args ...any) + +func main() { + logger := CreateLogger("tsukasa", Info) + + config, err := GetConfig() + if err != nil { + logger.Fatalf("invalid config: %v", err) + } + + if err := config.ValidateServices(); err != nil { + logger.Fatalf("invalid service config: %v", err) + } + + tsnet := new(tsnet.Server) + tsnet.Hostname = config.Tailscale.Hostname + 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 + if config.Tailscale.Verbose { + tsnet.Logf = tsnet.UserLogf + } else { + tsnet.Logf = func(format string, args ...any) {} + } + + shutdownCh := make(chan struct{}) + shutdownWg := &sync.WaitGroup{} + serviceContext := &ServiceContext{ + TsNet: tsnet, + ShutdownCh: shutdownCh, + ShutdownWg: shutdownWg, + } + + usingTailscale := false + var services []*Service + for name, serviceConfig := range config.Services { + service, err := CreateService(serviceContext, name, serviceConfig) + if err != nil { + logger.Fatalf("failed to create service %q: %v", name, err) + } + if service.ListenType == AddressTailscaleTCP || service.ConnectType == AddressTailscaleTCP { + usingTailscale = true + } + services = append(services, service) + } + + if usingTailscale { + if err := config.ValidateTailscaleConfig(); err != nil { + logger.Fatalf("Tailscale used but got invalid Tailscale config: %v", err) + } + if config.Tailscale.AuthKey == "" { + logger.Infof("Tailscale authkey not provided, will try interactive login") + } + if err := tsnet.Start(); err != nil { + logger.Fatalf("failed to start Tailscale: %v", err) + } + defer tsnet.Close() + } + + // Start services. + if len(services) == 0 { + logger.Fatalf("no services defined. run %s -h for help", os.Args[0]) + } + for _, service := range services { + go service.Start() + } + + // Wait for signal to shutdown. + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + <-c + close(shutdownCh) + shutdownWg.Wait() +} diff --git a/pipe.go b/pipe.go new file mode 100644 index 0000000..29be5da --- /dev/null +++ b/pipe.go @@ -0,0 +1,34 @@ +package main + +import ( + "io" + "net" + "sync/atomic" +) + +func PipeAndClose(conn net.Conn, targetConn net.Conn, logger *Logger) { + var closed uint32 = 0 + close := func() { + if atomic.CompareAndSwapUint32(&closed, 0, 1) { + conn.Close() + targetConn.Close() + } + } + + // Forward data between conn and targetConn. + go func() { + defer close() + + _, err := io.Copy(targetConn, conn) + if err != nil && atomic.LoadUint32(&closed) == 0 { + logger.Errorf("error copying data to target: %v\n", err) + } + conn.Close() + }() + + defer close() + + if _, err := io.Copy(conn, targetConn); err != nil && atomic.LoadUint32(&closed) == 0 { + logger.Errorf("error copying data from target: %v\n", err) + } +} diff --git a/service.go b/service.go new file mode 100644 index 0000000..bff8069 --- /dev/null +++ b/service.go @@ -0,0 +1,240 @@ +package main + +import ( + "context" + "fmt" + "net" + "net/url" + "os" + "strconv" + "sync" + + "tailscale.com/tsnet" +) + +type AddressType int + +const ( + AddressTCP AddressType = iota + AddressUNIXSocket + AddressTailscaleTCP +) + +type ServiceContext struct { + TsNet *tsnet.Server + ShutdownCh chan struct{} + ShutdownWg *sync.WaitGroup +} + +type Service struct { + ServiceContext *ServiceContext + Config *ServiceConfig + + Name string + ListenType AddressType + ListenAddress string + ListenPort int16 + ConnectType AddressType + ConnectAddress string + ConnectPort int16 + ConnectProxyProtocol bool + LogLevel LogLevel +} + +func parsePort(portString string) (int16, error) { + if portString == "" { + return 0, fmt.Errorf("empty port") + } else if port, err := strconv.ParseInt(portString, 10, 16); err != nil { + return 0, fmt.Errorf("invalid port") + } else { + return int16(port), nil + } +} + +type urlType string + +const ( + urlTypeListen urlType = "listen" + urlTypeConnect urlType = "connect" +) + +func parseUrl(urlType urlType, urlString string) (addressType AddressType, address string, port int16, e error) { + if url, err := url.Parse(urlString); err != nil { + e = fmt.Errorf("failed to parse %s URL: %v", urlType, err) + } else { + switch url.Scheme { + case "tcp": + if port, err = parsePort(url.Port()); err != nil { + e = fmt.Errorf("failed to parse %s port: %v", urlType, err) + } else { + addressType = AddressTCP + address = url.Hostname() + } + case "unix": + addressType = AddressUNIXSocket + address = url.Path + case "tailscale": + // Allowed ListenAddress for Tailscale is "::" or "0.0.0.0" + if urlType == urlTypeListen && (url.Hostname() != "::" && url.Hostname() != "0.0.0.0") { + e = fmt.Errorf("invalid Tailscale %s address: %s (only \"::\" and \"0.0.0.0\" allowed)", urlType, url.Hostname()) + } else if port, err = parsePort(url.Port()); err != nil { + e = fmt.Errorf("failed to parse %s port: %v", urlType, err) + } else { + addressType = AddressTailscaleTCP + address = url.Hostname() + } + default: + e = fmt.Errorf("unsupported %s URL scheme: %s", urlType, url.Scheme) + } + } + return +} + +func CreateService(serviceContext *ServiceContext, name string, config *ServiceConfig) (service *Service, err error) { + service = &Service{ + ServiceContext: serviceContext, + Config: config, + Name: name, + LogLevel: config.LogLevel, + ConnectProxyProtocol: config.ProxyProtocol, + } + if service.ListenType, service.ListenAddress, service.ListenPort, err = parseUrl(urlTypeListen, config.Listen); err != nil { + return nil, err + } + if service.ConnectType, service.ConnectAddress, service.ConnectPort, err = parseUrl(urlTypeConnect, config.Connect); err != nil { + return nil, err + } + return +} + +func (s *Service) Listen() (listener net.Listener, cleanup func(), err error) { + switch s.ListenType { + case AddressTCP: + listener, err = net.Listen("tcp", s.ListenAddress+":"+strconv.Itoa(int(s.ListenPort))) + cleanup = func() { + listener.Close() + } + case AddressUNIXSocket: + listener, err = net.Listen("unix", s.ListenAddress) + cleanup = func() { + listener.Close() + os.Remove(s.ListenAddress) + } + case AddressTailscaleTCP: + listener, err = s.ServiceContext.TsNet.Listen("tcp", ":"+strconv.Itoa(int(s.ListenPort))) + cleanup = func() { + listener.Close() + } + default: + return nil, nil, fmt.Errorf("invalid listen address type: %v", s.ListenType) + } + return +} + +func (s *Service) CreateConnector() (func() (net.Conn, error), error) { + switch s.ConnectType { + case AddressTCP: + return func() (net.Conn, error) { + return net.Dial("tcp", s.ConnectAddress+":"+strconv.Itoa(int(s.ConnectPort))) + }, nil + case AddressUNIXSocket: + return func() (net.Conn, error) { + return net.Dial("unix", s.ConnectAddress) + }, nil + case AddressTailscaleTCP: + return func() (net.Conn, error) { + return s.ServiceContext.TsNet.Dial(context.Background(), "tcp", s.ConnectAddress+":"+strconv.Itoa(int(s.ConnectPort))) + }, nil + default: + return nil, fmt.Errorf("invalid connect address type: %v", s.ConnectType) + } +} + +func (s *Service) Start() { + logger := CreateLogger("services/"+s.Name, s.LogLevel) + + listener, cleanup, err := s.Listen() + if err != nil { + logger.Errorf("failed to create listener: %v", err) + return + } + + connector, err := s.CreateConnector() + if err != nil { + logger.Errorf("failed to create connector: %v", err) + return + } + + logger.Infof("listening on %s", s.Config.Listen) + s.ServiceContext.ShutdownWg.Add(1) + + connCh := make(chan net.Conn) + go func() { + for { + conn, err := listener.Accept() + select { + case <-s.ServiceContext.ShutdownCh: + return + default: + if err != nil { + logger.Errorf("failed to accept connection: %v", err) + continue + } + logger.Verbosef("accepted connection from %v", conn.RemoteAddr()) + connCh <- conn + } + } + }() + + for { + select { + case <-s.ServiceContext.ShutdownCh: + cleanup() + s.ServiceContext.ShutdownWg.Done() + return + case conn := <-connCh: + go func() { + targetConn, err := connector() + if err != nil { + logger.Errorf("failed to connect to target: %v", err) + conn.Close() + return + } + logger.Verbosef("connected to target %v", targetConn.RemoteAddr()) + + if s.Config.ProxyProtocol { + varsion, remoteIp, remotePort := tryExtractAddr(conn.RemoteAddr()) + _, localIp, localPort := tryExtractAddr(conn.LocalAddr()) + header := fmt.Sprintf("PROXY TCP%d %s %s %d %d\r\n", varsion, remoteIp, localIp, remotePort, localPort) + logger.Verbosef("writing PROXY Protocol header: %v", header) + if _, err := targetConn.Write([]byte(header)); err != nil { + logger.Errorf("failed to write PROXY Protocol header: %v", err) + targetConn.Close() + conn.Close() + return + } + } + + PipeAndClose(conn, targetConn, logger) + }() + } + } +} + +func tryExtractAddr(addr net.Addr) (version int, ip string, port int) { + switch addr := addr.(type) { + case *net.TCPAddr: + if addr.IP.To4() == nil { + version = 6 + } else { + version = 4 + } + ip = addr.IP.String() + port = addr.Port + default: + version = 4 + ip = "0.0.0.0" + port = 0 + } + return +}