Skip to content

Commit b1bf492

Browse files
committed
Initial commit
0 parents  commit b1bf492

29 files changed

+3193
-0
lines changed

.github/workflows/lint.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: golangci-lint
2+
on:
3+
push:
4+
branches:
5+
- main
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
# Optional: allow read access to pull request. Use with `only-new-issues` option.
11+
# pull-requests: read
12+
13+
jobs:
14+
golangci:
15+
name: lint
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-go@v5
20+
with:
21+
go-version-file: 'go.mod'
22+
- name: golangci-lint
23+
uses: golangci/golangci-lint-action@v7
24+
with:
25+
version: v2.0

.github/workflows/test.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Go Test
2+
on:
3+
push:
4+
branches:
5+
- main
6+
pull_request:
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
- name: Setup Go
15+
uses: actions/setup-go@v5
16+
with:
17+
go-version-file: 'go.mod'
18+
- name: Build
19+
run: go build -v ./...
20+
- name: Test with the Go CLI
21+
run: go test ./...

.gitignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Added by goreleaser init:
2+
dist/
3+
4+
# If you prefer the allow list template instead of the deny list, see community template:
5+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
6+
#
7+
# Binaries for programs and plugins
8+
*.exe
9+
*.exe~
10+
*.dll
11+
*.so
12+
*.dylib
13+
14+
# Test binary, built with `go test -c`
15+
*.test
16+
17+
# Output of the go coverage tool, specifically when used with LiteIDE
18+
*.out
19+
20+
# Dependency directories (remove the comment below to include it)
21+
# vendor/
22+
23+
# Go workspace file
24+
go.work
25+
go.work.sum
26+
27+
# env file
28+
.env

.go-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.24.2

.goreleaser.yaml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# This is an example .goreleaser.yml file with some sensible defaults.
2+
# Make sure to check the documentation at https://goreleaser.com
3+
4+
# The lines below are called `modelines`. See `:help modeline`
5+
# Feel free to remove those if you don't want/need to use them.
6+
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
7+
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
8+
9+
version: 2
10+
11+
project_name: tsgrok
12+
13+
before:
14+
hooks:
15+
# You may remove this if you don't use go modules.
16+
- go mod tidy
17+
18+
builds:
19+
- env:
20+
- CGO_ENABLED=0
21+
goos:
22+
- linux
23+
- windows
24+
- darwin
25+
main: ./cmd/tsgrok
26+
binary: tsgrok
27+
28+
archives:
29+
- formats: [tar.gz]
30+
# this name template makes the OS and Arch compatible with the results of `uname`.
31+
name_template: >-
32+
{{ .ProjectName }}_
33+
{{- title .Os }}_
34+
{{- if eq .Arch "amd64" }}x86_64
35+
{{- else if eq .Arch "386" }}i386
36+
{{- else }}{{ .Arch }}{{ end }}
37+
{{- if .Arm }}v{{ .Arm }}{{ end }}
38+
# use zip for windows archives
39+
format_overrides:
40+
- goos: windows
41+
formats: [zip]
42+
43+
changelog:
44+
sort: asc
45+
filters:
46+
exclude:
47+
- "^docs:"
48+
- "^test:"
49+
50+
release:
51+
footer: >-
52+
53+
---
54+
55+
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).

.vscode/launch.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Launch Package",
9+
"type": "go",
10+
"request": "launch",
11+
"mode": "debug",
12+
"program": "${workspaceFolder}/cmd/tsgrok",
13+
"cwd": "${workspaceFolder}",
14+
"debugAdapter": "dlv-dap",
15+
"console": "integratedTerminal"
16+
}
17+
]
18+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 jonson
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# tsgrok ✨
2+
3+
[![Go Report Card](https://goreportcard.com/badge/github.com/[your-username]/tsgrok)](https://goreportcard.com/report/github.com/[your-username]/tsgrok)
4+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5+
6+
**Expose local http services using Tailscale via an interactive Terminal UI.**
7+
8+
`tsgrok` provides a convenient way to create and inspect [ephemeral](https://tailscale.com/kb/1111/ephemeral-nodes) [Tailscale Funnels](https://tailscale.com/kb/1223/funnel) directly from your terminal.
9+
10+
## Demo
11+
12+
![tsgrok demo](docs/tsgrok.gif)
13+
14+
15+
## Why `tsgrok`?
16+
17+
While the `tailscale funnel` command is powerful, managing multiple funnels is challenging. `tsgrok` takes a different approach, creating a disposable ephemeral node in your tsnet for each funnel you create, and destrorying it when the app is closed.
18+
19+
* **Visibility:** See all your funnels and their status at a glance.
20+
* **Ease of Use:** A dedicated TUI simplifies common actions like creation, deletion, and URL copying.
21+
* **Request Inspection:** Quickly check if traffic is hitting your funnel without needing separate logging.
22+
* **Disposable:** Each funnel is created on distinct ephemeral node, all of which live in memory and are destroyed when the app is terminated. Orphaned ephemeral nodes are automatically cleaned up by Tailscale.
23+
24+
## Requirements
25+
26+
* **Go:** Version 1.18 or higher (Update if you require a newer version).
27+
* **Tailscale:** You need Tailscale installed, configured, and running on your machine. `tsgrok` interacts with your local Tailscale daemon.
28+
29+
## Installation
30+
31+
### Releases
32+
See the releases page for the latest releases. Binaries are provided for major platforms.
33+
34+
### Go
35+
The easiest way to install `tsgrok` is using `go install`:
36+
37+
```bash
38+
go install github.com/jonson/tsgrok@latest
39+
```
40+
41+
Make sure your Go environment variables (`GOPATH`, `GOBIN`) are set up correctly so the installed binary is in your system's `PATH`.
42+
43+
## Usage
44+
45+
Simply run the command:
46+
47+
```bash
48+
tsgrok
49+
```
50+
51+
## Tailscale Auth
52+
53+
`tsgrok` is a standalone application that does not rely on Tailscale being installed on the machine running it. Rather, it relies on an auth key to
54+
provision and manage ephemeral nodes. You should provision an auth key, something like the following:
55+
56+
![Tailscale Auth Key Configuration](docs/image.png)
57+
58+
This must be set in the environment variable TSGROK_AUTHKEY, and it should be of the format `tskey-auth-...`.
59+
60+
Flags to consider when creating your auth key:
61+
* Tags: You can automatically tag any nodes created by tsgrok, and add further restrictions in the ACL if desired. Tags are not necessary however.
62+
* Ephemeral: `tsgrok` will only create ephemeral nodes, but you can toggle this to guarantee this is the case.
63+
* Reusable: This key nees to be reusable, else you will only be able to create one funnel!
64+
* Expiration: Set to whatever you desire.
65+
66+
## Contributing
67+
68+
Contributions are welcome! If you find a bug or have a feature request, please open an issue on GitHub. If you'd like to contribute code, please open a pull request.
69+
70+
## License
71+
72+
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.

cmd/tsgrok/main.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"sync"
7+
8+
tea "github.com/charmbracelet/bubbletea"
9+
"github.com/joho/godotenv"
10+
"github.com/jonson/tsgrok/internal/funnel"
11+
"github.com/jonson/tsgrok/internal/tui"
12+
"github.com/jonson/tsgrok/internal/util"
13+
)
14+
15+
func main() {
16+
serverErrorLog := util.NewServerErrorLog()
17+
18+
err := godotenv.Load()
19+
if err != nil {
20+
serverErrorLog.Println("Error loading .env file")
21+
}
22+
23+
if util.GetAuthKey() == "" {
24+
fmt.Printf("Missing env var %s, please set it and try again.\n", util.AuthKeyEnvVar)
25+
os.Exit(1)
26+
}
27+
28+
messageBus := &util.MessageBusImpl{}
29+
funnelRegistry := funnel.NewFunnelRegistry()
30+
httpServer := funnel.NewHttpServer(util.GetProxyHttpPort(), messageBus, funnelRegistry, serverErrorLog)
31+
32+
m := tui.InitialModel(funnelRegistry, serverErrorLog)
33+
34+
go func() {
35+
err := httpServer.Start()
36+
if err != nil {
37+
// channel to send error to main thread
38+
fmt.Fprintf(os.Stderr, "Error starting HTTP server: %v\n", err)
39+
}
40+
}()
41+
42+
cleanup := func() {
43+
if len(funnelRegistry.Funnels) == 0 {
44+
return
45+
}
46+
47+
if len(funnelRegistry.Funnels) == 1 {
48+
fmt.Printf("Closing tunnel\n")
49+
} else {
50+
fmt.Printf("Closing %d tunnels\n", len(funnelRegistry.Funnels))
51+
}
52+
53+
var wg sync.WaitGroup
54+
for _, f := range funnelRegistry.Funnels {
55+
wg.Add(1)
56+
go func(f funnel.Funnel) {
57+
defer wg.Done()
58+
err := f.Destroy()
59+
// should we log it?
60+
if err != nil {
61+
fmt.Printf("Error destroying tunnel: %v\n", err)
62+
}
63+
}(f)
64+
}
65+
wg.Wait()
66+
}
67+
defer cleanup()
68+
69+
p := tea.NewProgram(m, tea.WithAltScreen())
70+
71+
messageBus.SetProgram(p)
72+
73+
if _, err := p.Run(); err != nil {
74+
fmt.Println(err)
75+
}
76+
77+
}

docs/image.png

100 KB
Loading

0 commit comments

Comments
 (0)