From 105e5e7fd236f8b9a70e1ae28cf42d28f995b1c2 Mon Sep 17 00:00:00 2001 From: Fabrizio Gomez Date: Fri, 8 May 2026 22:26:00 -0600 Subject: [PATCH 1/8] Avoid panics by unexpected EOF --- cmd/yatun/main.go | 9 +++++---- internal/server/server.go | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/yatun/main.go b/cmd/yatun/main.go index 2d991a9..6a9c4e7 100644 --- a/cmd/yatun/main.go +++ b/cmd/yatun/main.go @@ -60,12 +60,13 @@ func clientServerComms(ses *yamux.Session, tuiP *tea.Program) { } func initializeServerConnection(tuiP *tea.Program) (sess *yamux.Session, err error) { - conn, err := net.Dial("tcp", "0.0.0.0:5678") + conn, err := net.Dial("tcp", "yatun.snowdev.one:5678") if err != nil { return } - - sess, err = yamux.Client(conn, nil) + c := yamux.DefaultConfig() + c.LogOutput = io.Discard + sess, err = yamux.Client(conn, c) if err != nil { return } @@ -79,7 +80,7 @@ func serverConnectionLoop(sess *yamux.Session, port *string, tuiP *tea.Program) // TODO: After initial handshake is done, io.Copy from server (yatun) to internal target server for { - stream, err := sess.Accept() + stream, err := sess.AcceptStream() if err != nil { log.Printf("Error accepting new stream \n%v", err) // TODO: Maybe? send a message to the TUI so that the user knows it is having trouble getting new sessions from server diff --git a/internal/server/server.go b/internal/server/server.go index 5080983..4fb7fa7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -164,8 +164,8 @@ func (s *ServerConnection) StartListeningAgents() error { // This means the agent stream disconnected, it is fine to just break here return errors.New("sess disconnected") } - - panic(err) + log.Printf("Unrecognized err: %v", err) + return err } // When we receive an agent stream, listen and parse for the first handshake and then we can start copying over / creating new sessions From 1bf126955a45e0bbaaa27aac73ba79750c2e570c Mon Sep 17 00:00:00 2001 From: Fabrizio Gomez Date: Fri, 8 May 2026 23:42:39 -0600 Subject: [PATCH 2/8] Read server side domain from env --- cmd/yatun/main.go | 2 +- cmd/yatund/main.go | 4 +++- internal/config/config.go | 32 ++++++++++++++++++++++++++++++++ internal/server/server.go | 18 ++++++++++++++++-- 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 internal/config/config.go diff --git a/cmd/yatun/main.go b/cmd/yatun/main.go index 6a9c4e7..77ef865 100644 --- a/cmd/yatun/main.go +++ b/cmd/yatun/main.go @@ -60,7 +60,7 @@ func clientServerComms(ses *yamux.Session, tuiP *tea.Program) { } func initializeServerConnection(tuiP *tea.Program) (sess *yamux.Session, err error) { - conn, err := net.Dial("tcp", "yatun.snowdev.one:5678") + conn, err := net.Dial("tcp", "0.0.0.0:5678") if err != nil { return } diff --git a/cmd/yatund/main.go b/cmd/yatund/main.go index cbcc0da..aced3e8 100644 --- a/cmd/yatund/main.go +++ b/cmd/yatund/main.go @@ -3,10 +3,12 @@ package main import ( "net" + "github.com/KatIsCoding/yatun/internal/config" "github.com/KatIsCoding/yatun/internal/server" ) func main() { + conf := config.ReadFromEnv() // So, we need to open a tcp server for external connections and another one for the agent. // But the external should only open if an agent requests it, so the very first server is the agent one. @@ -24,7 +26,7 @@ func main() { panic(err) } - sconn, err := server.NewServerConnection(conn) + sconn, err := server.NewServerConnection(conn, conf) if err != nil { panic(err) } diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..4a3149b --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,32 @@ +package config + +import ( + "log" + + "net/url" + "strings" + + "os" +) + +type ServerConfig struct { + Domain *string +} + +func ReadFromEnv() ServerConfig { + c := ServerConfig{} + domain, ok := os.LookupEnv("Domain") + if ok { + if !strings.Contains(domain, "//") { + domain = "//" + domain + } + u, err := url.Parse(domain) + if err != nil { + panic(err) + } + log.Printf("Host: %v", u.Hostname()) + c.Domain = new(u.Hostname()) + } + + return c +} diff --git a/internal/server/server.go b/internal/server/server.go index 4fb7fa7..9fe22b0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,12 +2,14 @@ package server import ( "errors" + "fmt" "io" "log" "net" "sync" "time" + "github.com/KatIsCoding/yatun/internal/config" "github.com/KatIsCoding/yatun/internal/message" "github.com/hashicorp/yamux" ) @@ -16,6 +18,7 @@ type ServerConnection struct { agentSession *yamux.Session connectionType *message.MessageType connectionDetails *message.ConnectionDetailsMessageData + config config.ServerConfig } func (s *ServerConnection) handleInitialConfig(stream *yamux.Stream) error { @@ -118,11 +121,21 @@ func (s *ServerConnection) handleExternalConnections(serv net.Listener) error { } func (s *ServerConnection) sendAddressInfo(stream *yamux.Stream, server net.Listener) error { + addr := server.Addr().String() + if s.config.Domain != nil { + _, port, err := net.SplitHostPort(server.Addr().String()) + if err != nil { + panic(err) + } + + addr = fmt.Sprintf("%v:%v", *s.config.Domain, port) + } + m := message.TransportMessage{ Type: message.ResponseMessage, Data: &message.Response{ Ok: true, - Address: server.Addr().String(), + Address: addr, }, } @@ -205,7 +218,7 @@ func (s *ServerConnection) StartListeningAgents() error { return nil } -func NewServerConnection(conn net.Conn) (*ServerConnection, error) { +func NewServerConnection(conn net.Conn, conf config.ServerConfig) (*ServerConnection, error) { sess, err := yamux.Server(conn, yamux.DefaultConfig()) if err != nil { return nil, err @@ -213,5 +226,6 @@ func NewServerConnection(conn net.Conn) (*ServerConnection, error) { return &ServerConnection{ agentSession: sess, connectionType: nil, + config: conf, }, nil } From 89cc7fb7d8fe7858162b7b3e5e29b2035d267948 Mon Sep 17 00:00:00 2001 From: Fabrizio Gomez Date: Fri, 8 May 2026 23:44:55 -0600 Subject: [PATCH 3/8] The agent now accepts a --server that allows it to point to another server if wanted --- cmd/yatun/main.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/yatun/main.go b/cmd/yatun/main.go index 77ef865..18b8f55 100644 --- a/cmd/yatun/main.go +++ b/cmd/yatun/main.go @@ -59,8 +59,8 @@ func clientServerComms(ses *yamux.Session, tuiP *tea.Program) { }() } -func initializeServerConnection(tuiP *tea.Program) (sess *yamux.Session, err error) { - conn, err := net.Dial("tcp", "0.0.0.0:5678") +func initializeServerConnection(tuiP *tea.Program, server string) (sess *yamux.Session, err error) { + conn, err := net.Dial("tcp", fmt.Sprintf("%v:5678", server)) if err != nil { return } @@ -129,6 +129,7 @@ func serverConnectionLoop(sess *yamux.Session, port *string, tuiP *tea.Program) func main() { port := flag.String("port", "", "--port") + server := flag.String("server", "yatun.snowdev.one", "--server") flag.Parse() if port == nil || len(*port) == 0 { @@ -138,7 +139,7 @@ func main() { tuiP := tui.BuildTUI() //TODO: More info needs to be passed to the server so that it knows to which subdomain to match - sess, err := initializeServerConnection(tuiP) + sess, err := initializeServerConnection(tuiP, *server) if err != nil { go tuiP.Send(tui.SetState{ Err: err, From 7f5e2b6898e864a4fc4eeea5dbcdb74bccbed6e7 Mon Sep 17 00:00:00 2001 From: Fabrizio Gomez Date: Fri, 8 May 2026 23:47:42 -0600 Subject: [PATCH 4/8] Useful log for server to display where is it logging --- cmd/yatund/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/yatund/main.go b/cmd/yatund/main.go index aced3e8..d8ce9fb 100644 --- a/cmd/yatund/main.go +++ b/cmd/yatund/main.go @@ -1,6 +1,7 @@ package main import ( + "log" "net" "github.com/KatIsCoding/yatun/internal/config" @@ -19,6 +20,8 @@ func main() { panic(err) } + log.Printf("Listening on %v", listener.Addr().String()) + for { conn, err := listener.Accept() From 2991847befd3a913bab322ba733d5d1e49f12a9d Mon Sep 17 00:00:00 2001 From: Fabrizio Gomez Date: Fri, 8 May 2026 23:50:52 -0600 Subject: [PATCH 5/8] Correct issue of terminal just being in a locked state if the server closes while the agent is connected --- cmd/yatun/main.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/yatun/main.go b/cmd/yatun/main.go index 18b8f55..36e2b7a 100644 --- a/cmd/yatun/main.go +++ b/cmd/yatun/main.go @@ -1,10 +1,11 @@ package main import ( + "errors" "flag" "fmt" "io" - "log" + "os" "sync" "time" @@ -42,7 +43,8 @@ func clientServerComms(ses *yamux.Session, tuiP *tea.Program) { for { msg, err := message.Decode(con) if err != nil { - panic(err) + tuiP.Kill() + panic(errors.New("the server closed unexpectedly")) } switch msg.Type { @@ -82,7 +84,6 @@ func serverConnectionLoop(sess *yamux.Session, port *string, tuiP *tea.Program) stream, err := sess.AcceptStream() if err != nil { - log.Printf("Error accepting new stream \n%v", err) // TODO: Maybe? send a message to the TUI so that the user knows it is having trouble getting new sessions from server tuiP.Send(tui.SetState{ Err: err, @@ -155,7 +156,7 @@ func main() { } if _, err := tuiP.Run(); err != nil { - fmt.Printf("Cya %v", err) + sess.Close() os.Exit(1) } From 4a1c4a486c2f0db38ccb987344acadfe1353aeb1 Mon Sep 17 00:00:00 2001 From: Fabrizio Gomez Date: Fri, 8 May 2026 23:55:48 -0600 Subject: [PATCH 6/8] Better error codes for the agent --- cmd/yatun/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/yatun/main.go b/cmd/yatun/main.go index 36e2b7a..499f1b3 100644 --- a/cmd/yatun/main.go +++ b/cmd/yatun/main.go @@ -27,7 +27,8 @@ func sendMsg(con *yamux.Stream, m message.TransportMessage) { func clientServerComms(ses *yamux.Session, tuiP *tea.Program) { con, err := ses.OpenStream() if err != nil { - panic(err) + tuiP.Kill() + panic(errors.New("failed to accept new stream, is the server running?")) } dat := message.ConnectionDetailsMessageData{ @@ -134,7 +135,9 @@ func main() { flag.Parse() if port == nil || len(*port) == 0 { - panic("no port") + fmt.Printf("The --port parameter is missing\n") + flag.Usage() + return } tuiP := tui.BuildTUI() From 2ef6c0f0bece5b229fcc0f4b3f3dbfdb4ea57fad Mon Sep 17 00:00:00 2001 From: Fabrizio Gomez Date: Sat, 9 May 2026 00:05:30 -0600 Subject: [PATCH 7/8] Clarification of ssl --- README.md | 116 +++++++++++++++++++++++++++++++++++++++++++--- cmd/yatun/main.go | 1 - 2 files changed, 109 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3278c0e..b16f0d8 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,117 @@ # yatun -Project structure: +**yatun** is an open-source TCP reverse tunneling tool (like ngrok/cloudflared) that exposes local services to the internet. It consists of a public relay server and a lightweight agent. +``` +┌──────────────┐ TCP ┌──────────────┐ yamux ┌──────────────┐ TCP ┌─────────┐ +│ Internet │──────────▶│ yatund │◀══════════▶│ yatun │────────▶│ local │ +│ Client │ :ephemeral│ (server) │ session │ (agent) │ :port │ service │ +└──────────────┘ └──────────────┘ └──────────────┘ └─────────┘ +``` +## Architecture + +- **`yatund`** — The public relay server. Agents connect to it on port `5678`. For each agent, it opens an ephemeral TCP port on the server's public interface and forwards all incoming connections through a yamux session back to the agent. + +- **`yatun`** — The agent (client) that runs alongside your local service. It connects to the yatund server, authenticates, and starts forwarding traffic. Features a real-time Bubble Tea TUI showing tunnel status, connection counts, and ping latency. + +### How it works + +1. The agent opens a TCP connection to `yatund:5678` and upgrades it to a yamux multiplexed session. +2. The agent sends connection details (subdomain name) over the first yamux stream. +3. The server opens an ephemeral TCP port, sends the public address back to the agent, and starts a heartbeat ping loop. +4. When an internet client connects to `:`, the server opens a new yamux stream to the agent. +5. The agent connects to `localhost:` and bidirectionally copies data between the yamux stream and the local service. + +## Security / TLS + +By default all connections are **plain TCP with no encryption** — the link between the internet client and yatund, and the link between yatund and the agent, are both unencrypted. + +There were too many ways to implement TLS (terminate at yatund, pass through, wrap the agent session, etc.) and none felt like an obvious clear choice, so it's left unimplemented for now. The practical workaround: put a **reverse proxy that handles TLS in front of yatund**. If you already run something like **Traefik**, **Caddy**, **nginx**, or **Cloudflare** (orange cloud), the TLS termination happens before traffic reaches yatund — and your setup is secure without any changes to yatun itself. + +## Getting Started + +### Prerequisites + +- Go 1.26+ +- A publicly accessible server for `yatund` + +### Running the server + +```bash +# Set your public domain (optional — used in address responses) +export DOMAIN=https://tunnel.example.com + +go run ./cmd/yatund/ +``` + +The server listens on port `5678` for agent connections. + +### Running an agent + +```bash +# Expose local port 8080 through the tunnel +go run ./cmd/yatun/ --port 8080 --server yatund.example.com +``` + +The agent opens a TUI showing connection status, the public address, active connections, and ping latency. Press `q` or `Ctrl+C` to quit, `c` to copy the public address to your clipboard. + +### Deploying with Docker + +```bash +docker compose -f deploy/docker-compose.yml up --build +``` + +## CLI Flags + +### Agent (`yatun`) + +| Flag | Default | Description | +|------------|-------------------------|------------------------------------| +| `--port` | (required) | Local port to expose | +| `--server` | `yatun.snowdev.one` | yatund server address | + +### Server (`yatund`) + +| Environment Variable | Description | +|----------------------|------------------------------------------| +| `DOMAIN` | Public-facing domain (used in responses) | + +## Wire Protocol + +Messages between agent and server use a simple binary format: + +``` +[1 byte: type][2 bytes: payload length (BE uint16)][N bytes: JSON payload] +``` + +| Type Byte | Message | Description | +|-----------|--------------------|------------------------------------| +| `A` | TCPConnection | Signal: agent wants TCP tunnel | +| `B` | ConnectionDetails | Handshake: subdomain info (JSON) | +| `C` | ResponseMessage | Server response with address (JSON)| +| `D` | PingMessage | Heartbeat from server (JSON) | + +## Development + +``` yatun/ -├── go.mod -├── go.sum -├── README.md -├── Makefile ├── cmd/ -│ ├── yatund/ # the server binary +│ ├── yatun/ # Agent/client binary │ │ └── main.go -│ └── yatun/ # the agent/client binary +│ └── yatund/ # Server binary │ └── main.go +├── deploy/ +│ ├── Dockerfile +│ └── docker-compose.yml +├── internal/ +│ ├── config/ # Server configuration (env vars) +│ ├── message/ # Wire protocol encoding/decoding +│ ├── server/ # Server tunneling logic +│ └── tui/ # Bubble Tea TUI for agent +└── web/ # (reserved) +``` + +## License + +MIT diff --git a/cmd/yatun/main.go b/cmd/yatun/main.go index 499f1b3..74e04c0 100644 --- a/cmd/yatun/main.go +++ b/cmd/yatun/main.go @@ -159,7 +159,6 @@ func main() { } if _, err := tuiP.Run(); err != nil { - sess.Close() os.Exit(1) } From 6dee113a904a88a5a221fc0ea193a6a840fc5e94 Mon Sep 17 00:00:00 2001 From: Fabrizio Gomez Date: Sat, 9 May 2026 00:11:25 -0600 Subject: [PATCH 8/8] CI/CD jobs --- .github/workflows/build.yml | 57 +++++++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f9a1421 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,57 @@ +name: Build + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + +jobs: + build: + strategy: + matrix: + arch: [amd64, arm64] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.26' + cache: true + + - name: Build yatund + run: | + GOOS=linux GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o build/yatund-${{ matrix.arch }} ./cmd/yatund/ + + - name: Build yatun + run: | + GOOS=linux GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o build/yatun-${{ matrix.arch }} ./cmd/yatun/ + + - uses: actions/upload-artifact@v4 + with: + name: binaries-${{ matrix.arch }} + path: build/ + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Collect binaries + run: | + mkdir release + find artifacts -type f -exec cp {} release/ \; + + - uses: softprops/action-gh-release@v2 + with: + draft: true + files: release/* + generate_release_notes: true diff --git a/README.md b/README.md index b16f0d8..43db4aa 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# yatun +# yatun | Yet another tunel **yatun** is an open-source TCP reverse tunneling tool (like ngrok/cloudflared) that exposes local services to the internet. It consists of a public relay server and a lightweight agent.