Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ jobs:
build:
strategy:
matrix:
arch: [amd64, arm64]
include:
- os: linux
arch: amd64
- os: linux
arch: arm64
- os: windows
arch: amd64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -20,16 +26,20 @@ jobs:
cache: true

- name: Build yatund
env:
EXE: ${{ matrix.os == 'windows' && '.exe' || '' }}
run: |
GOOS=linux GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o build/yatund-${{ matrix.arch }} ./cmd/yatund/
GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o build/yatund-${{ matrix.os }}-${{ matrix.arch }}${EXE} ./cmd/yatund/

- name: Build yatun
env:
EXE: ${{ matrix.os == 'windows' && '.exe' || '' }}
run: |
GOOS=linux GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o build/yatun-${{ matrix.arch }} ./cmd/yatun/
GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o build/yatun-${{ matrix.os }}-${{ matrix.arch }}${EXE} ./cmd/yatun/

- uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.arch }}
name: binaries-${{ matrix.os }}-${{ matrix.arch }}
path: build/

release:
Expand Down
108 changes: 41 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,57 @@

---

## Quick start
## Installation

### Prerequisites
### Via Go install

- Go 1.26+
- A server with a public IP (for yatund)
```bash
# For the client
go install github.com/KatIsCoding/yatun/cmd/yatun@latest
# For the server
go install github.com/KatIsCoding/yatun/cmd/yatund@latest
```

### Run the server
### Download binary

```bash
# Optional: set your public domain so addresses are reported as domain.com:port
export DOMAIN=https://tunnel.example.com
Pre-built binaries are available on the [Releases](https://github.com/KatIsCoding/yatun/releases) page.

go run ./cmd/yatund/
```
---

The server listens on port `5678` for agent connections.
## Quick start

### Run the agent
### Client (agent)

Expose a local service through the public relay at `yatun.snowdev.one` — no server setup needed.

```bash
# Expose local port 8080 through the tunnel
go run ./cmd/yatun/ --port 8080 --server your-server.com
# Expose local port 8080
yatun --port 8080
```

The agent opens a TUI showing connection status, the public address, active connections, and ping latency.
- Press `q` or `Ctrl+C` to quit.
- Press `c` to copy the public address to your clipboard.

Use `--server` to point at a different relay:

```bash
yatun --port 8080 --server your-server.com
```

### Server (relay)

Run your own relay on a machine with a public IP.

```bash
# Optional: set your public domain so addresses are reported as domain.com:port
export DOMAIN=https://tunnel.example.com

yatund
```

The server listens on port `5678` for agent connections.

---

## Configuration
Expand All @@ -65,6 +87,8 @@ The agent opens a TUI showing connection status, the public address, active conn
|----------|----------|---------|-------------|
| `DOMAIN` | No | `""` | Public-facing domain. When set, the server reports addresses as `yourdomain.com:<port>` instead of raw IP:port. |
| `TLS` | No | `0` (off) | Enable TLS for incoming external connections. Set to any non‑`0` value. Requires certificate files. |
| `CERT_PATH` | No | `certs/cert.cer` | Path to the TLS certificate (fullchain). Only used when `TLS` is enabled. |
| `KEY_PATH` | No | `certs/cert_key.key` | Path to the TLS private key. Only used when `TLS` is enabled. |

### Agent flags

Expand All @@ -77,7 +101,7 @@ The agent opens a TUI showing connection status, the public address, active conn

## Docker deployment

A Dockerfile and compose file are provided for deploying the server.
A Dockerfile and compose file are provided for a simple example of how the relay could be deployed with docker.

```bash
cd deploy
Expand All @@ -93,62 +117,12 @@ DOMAIN=https://tunnel.example.com
# TLS (set to 1 to enable)
TLS=1

# Paths to your certificate files on the host
# Paths to your certificate files on the host (For the purposes of the example in the deploy/ folder)
HOST_CER_FILE=/root/.acme.sh/example.com_ecc/fullchain.cer
HOST_KEY_FILE=/root/.acme.sh/example.com_ecc/example.com.key
```

The server looks for certificates at `certs/cert.cer` (fullchain) and `certs/cert_key.key` (private key) inside the container. The compose file maps host paths to those locations.

---

## TLS setup

yatund supports conditional TLS on external connections. When `TLS=1` is set, the server inspects the first byte of each incoming connection. If it's `0x16` (TLS handshake), the connection is transparently upgraded to TLS. Plain TCP connections are forwarded as-is on the same port.

### Obtaining certificates with acme.sh

Install acme.sh (a single-command helper is provided):

```bash
./ssl/install.sh your@email.com
```

Issue a certificate using DNS validation (Cloudflare example):

```bash
export CF_Token="your_cloudflare_api_token"
export CF_Zone_ID="your_zone_id"

acme.sh --issue --dns dns_cf -d yourdomain.com --server letsencrypt
```

> `--server letsencrypt` uses the **production** Let's Encrypt endpoint. You must include this flag — the default may be staging, which browsers do not trust. If you see `stg` in the CA Issuers URL inside your certificate, you used the staging server. Re-issue with `--server letsencrypt`.

After issuance, acme.sh prints the certificate paths. Your files will be under something like:

```
~/.acme.sh/yourdomain.com_ecc/
├── yourdomain.com.key # private key
├── yourdomain.com.cer # leaf certificate only
└── fullchain.cer # full chain (leaf + intermediates) ← use this
```

Make yatund point to `fullchain.cer` (not the leaf-only `.cer`) and the `.key` file. The server must serve the full certificate chain or browsers will reject it.

### Testing your certificate

```bash
# Check the certificate chain contains intermediates
openssl x509 -in fullchain.cer -text -noout | grep "CA Issuers"

# Verify the key matches the certificate
openssl x509 -noout -pubkey -in fullchain.cer | openssl md5
openssl ec -noout -pubkey -in yourdomain.com.key | openssl md5 # ECC key
openssl rsa -noout -pubkey -in yourdomain.com.key | openssl md5 # RSA key
```

The two hashes must match.
The server looks for certificates at `certs/cert.cer` (fullchain) and `certs/cert_key.key` (private key) inside the container by default (can be changed over env vars). The compose file maps host paths to those locations.

---

Expand Down
15 changes: 14 additions & 1 deletion internal/tls/tlsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/tls"
"log"
"net"
"os"
)

type TLSStore struct {
Expand All @@ -21,7 +22,19 @@ func (t TLSStore) Wrap(conn net.Conn) (*tls.Conn, error) {

}
func LoadTLSCerts() (TLSStore, error) {
cert, err := tls.LoadX509KeyPair("certs/cert.cer", "certs/cert_key.key")
cerLoc := "certs/cert.cer"
keyLoc := "certs/cert_key.key"

if envCL, ok := os.LookupEnv("CERT_PATH"); ok {
cerLoc = envCL
}
if envK, ok := os.LookupEnv("KEY_PATH"); ok {
cerLoc = envK
}

log.Printf("Reading cert from %v and key from %v", cerLoc, keyLoc)

cert, err := tls.LoadX509KeyPair(cerLoc, keyLoc)
if err != nil {
log.Printf("Failed to load certificates: %v", err)
return TLSStore{}, err
Expand Down
Loading