62 Commits

Author SHA1 Message Date
9276430fae refactor(session): add registry to manage SSH sessions
All checks were successful
renovate / renovate (push) Successful in 36s
Docker Build and Push / build-and-push-branches (push) Successful in 4m41s
Docker Build and Push / build-and-push-tags (push) Successful in 4m38s
- Implement thread-safe session registry with sync.RWMutex
- Add Registry interface for session management operations
- Support Get, Register, Update, and Remove session operations
- Enable dynamic slug updates for existing sessions
- Fix Connection closed by remote because HandleTCPIPForward run on a goroutine
2025-12-31 18:33:47 +07:00
f8a6f0bafe refactor(session): add registry to manage SSH sessions
All checks were successful
renovate / renovate (push) Successful in 39s
Docker Build and Push / build-and-push-branches (push) Successful in 4m27s
Docker Build and Push / build-and-push-tags (push) Successful in 4m22s
- Implement thread-safe session registry with sync.RWMutex
- Add Registry interface for session management operations
- Support Get, Register, Update, and Remove session operations
- Enable dynamic slug updates for existing sessions
2025-12-31 17:47:35 +07:00
acd02aadd3 refactor: restructure project architecture
All checks were successful
renovate / renovate (push) Successful in 45s
Docker Build and Push / build-and-push-branches (push) Successful in 5m54s
Docker Build and Push / build-and-push-tags (push) Successful in 6m21s
2025-12-31 15:49:37 +07:00
878664e0ac update: multi version build
All checks were successful
renovate / renovate (push) Successful in 35s
Docker Build and Push / build-and-push-branches (push) Successful in 6m7s
Docker Build and Push / build-and-push-tags (push) Successful in 6m6s
2025-12-31 13:48:36 +07:00
20a88df330 update: multi version build
All checks were successful
Docker Build and Push / build-and-push-tags (push) Has been skipped
renovate / renovate (push) Successful in 38s
Docker Build and Push / build-and-push-branches (push) Successful in 4m45s
2025-12-31 13:32:16 +07:00
075dd7ecad feat: add versioning system
Some checks failed
renovate / renovate (push) Successful in 38s
Docker Build and Push / build-and-push-branches (push) Has been skipped
Docker Build and Push / build-and-push-tags (push) Has been cancelled
2025-12-31 12:31:31 +07:00
ab34b34765 fix: prevent subdomain change to already-in-use subdomains
All checks were successful
renovate / renovate (push) Successful in 35s
Docker Build and Push / build-and-push (push) Successful in 5m42s
2025-12-30 19:41:33 +07:00
514c4f9de1 Merge branch 'staging' of https://git.fossy.my.id/bagas/tunnel-please into staging
All checks were successful
renovate / renovate (push) Successful in 20s
Docker Build and Push / build-and-push (push) Successful in 3m35s
2025-12-30 00:09:36 +07:00
d8330c684f feat: make SSH interaction UI fully responsive 2025-12-30 00:09:18 +07:00
fbf182025b Merge pull request 'main' (#52) from main into staging
All checks were successful
renovate / renovate (push) Successful in 21s
Reviewed-on: #52
2025-12-29 14:58:10 +00:00
1038c0861e Merge pull request 'chore(config): migrate Renovate config' (#51) from renovate/migrate-config into main
Reviewed-on: #51
2025-12-29 14:57:45 +00:00
64e0d5805e chore(config): migrate config renovate.json 2025-12-29 14:57:09 +00:00
85f21e7698 feat(tui): update interaction layer to Bubble Tea TUI
All checks were successful
renovate / renovate (push) Successful in 27s
Docker Build and Push / build-and-push (push) Successful in 3m49s
2025-12-29 21:55:39 +07:00
08565d845f Merge pull request 'staging' (#50) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 3m26s
Reviewed-on: #50
2025-12-29 10:17:00 +00:00
a7d9b2ab8a update: renovate target pr to staging branch
All checks were successful
renovate / renovate (push) Successful in 20s
2025-12-29 17:15:42 +07:00
bc8c5127a6 Merge pull request 'main' (#49) from main into staging
All checks were successful
renovate / renovate (push) Successful in 23s
Docker Build and Push / build-and-push (push) Successful in 3m26s
Reviewed-on: #49
2025-12-29 10:14:30 +00:00
a49b53e56f Merge pull request 'chore(deps): update actions/checkout action to v6' (#48) from renovate/actions-checkout-6.x into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 1m23s
Reviewed-on: #48
2025-12-29 10:04:10 +00:00
e5b5cc3ae5 chore(deps): update actions/checkout action to v6 2025-12-29 10:03:19 +00:00
b0b00764cf Update .gitea/workflows/renovate.yml
All checks were successful
renovate / renovate (push) Successful in 23s
2025-12-29 10:02:56 +00:00
8b6cdef2e9 Merge pull request 'chore(config): migrate Renovate config' (#47) from renovate/migrate-config into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 1m25s
Reviewed-on: #47
2025-12-29 09:49:10 +00:00
653517f5be chore(config): migrate config renovate.json 2025-12-29 09:48:12 +00:00
f11a92fb3b ci: configure workflow to ignore non-Go file changes
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 3m30s
renovate / renovate (push) Successful in 22s
2025-12-29 15:58:24 +07:00
ac283626d3 docs: add Docker deployment section 2025-12-29 15:57:31 +07:00
ad7c5985b1 feat: add docker compose deployment configurations 2025-12-29 15:57:16 +07:00
2644b4521c refactor: improve encapsulation
All checks were successful
renovate / renovate (push) Successful in 20s
Docker Build and Push / build-and-push (push) Successful in 3m25s
2025-12-29 12:37:03 +07:00
d23ed27a4a Merge pull request 'staging' (#46) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 1m21s
renovate / renovate (push) Successful in 21s
Reviewed-on: #46
2025-12-28 13:19:25 +00:00
b5862bd7a0 feat: add configurable HTTP port
All checks were successful
renovate / renovate (push) Successful in 20s
Docker Build and Push / build-and-push (push) Successful in 1m45s
2025-12-28 20:09:31 +07:00
bf7f7bd8da feat: add configurable HTTPS port
All checks were successful
renovate / renovate (push) Successful in 19s
Docker Build and Push / build-and-push (push) Successful in 1m24s
2025-12-28 20:03:49 +07:00
c3a469be64 refactor: use relative paths for certificates instead of absolute paths
All checks were successful
renovate / renovate (push) Successful in 19s
Docker Build and Push / build-and-push (push) Successful in 1m32s
2025-12-28 19:53:03 +07:00
eee04daf80 feat: optimize Docker build for production
All checks were successful
renovate / renovate (push) Successful in 19s
Docker Build and Push / build-and-push (push) Successful in 1m30s
2025-12-28 19:29:32 +07:00
1d918ef2aa feat(port): disable TCP forwarding by default and refactor port manager
All checks were successful
renovate / renovate (push) Successful in 28s
Docker Build and Push / build-and-push (push) Successful in 5m27s
2025-12-28 19:03:26 +07:00
a2676a4f30 Merge branch 'staging' of https://git.fossy.my.id/bagas/tunnel-please into staging
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 3m36s
renovate / renovate (push) Successful in 20s
2025-12-28 18:45:46 +07:00
83657d3206 refactor: remove unnecessary caching of environment data 2025-12-28 18:45:22 +07:00
6710aec4bf Merge pull request 'main' (#45) from main into staging
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 3m33s
renovate / renovate (push) Successful in 19s
Reviewed-on: #45
2025-12-28 08:08:58 +00:00
0ca6285ef5 Update .gitea/workflows/renovate.yml
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 3m32s
2025-12-28 08:08:10 +00:00
28cc069fdb Merge pull request 'chore(deps): update golang docker tag to v1.25.5' (#42) from renovate/golang-1.x into main
Some checks failed
Docker Build and Push / build-and-push (push) Has been cancelled
renovate / renovate (push) Successful in 21s
Reviewed-on: #42
2025-12-28 08:07:44 +00:00
fe58e35e91 Merge pull request 'fix(deps): update module golang.org/x/crypto to v0.46.0' (#43) from renovate/golang.org-x-crypto-0.x into main
Some checks failed
renovate / renovate (push) Has been cancelled
Docker Build and Push / build-and-push (push) Has been cancelled
Reviewed-on: #43
2025-12-28 08:07:31 +00:00
6cac64412c fix(deps): update module golang.org/x/crypto to v0.46.0 2025-12-28 08:01:56 +00:00
318003ac9f chore(deps): update golang docker tag to v1.25.5 2025-12-28 08:01:36 +00:00
14fa237027 Merge pull request 'staging' (#41) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 3m32s
renovate / renovate (push) Successful in 45s
Reviewed-on: #41
2025-12-28 07:55:00 +00:00
9a2a373eb3 feat: add renovate
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 6m3s
2025-12-28 14:51:00 +07:00
2a43f1441c Merge pull request 'fix: potential resource leak' (#38) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 4m27s
Reviewed-on: #38
2025-12-17 14:38:40 +00:00
5c6826fe89 Merge pull request 'fix: add nil check in Forwarder.Close to avoid panic (issue #36)' (#37) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 4m9s
Reviewed-on: #37
2025-12-12 08:36:51 +00:00
b484981017 Merge pull request 'staging' (#35) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 4m22s
Reviewed-on: #35
2025-12-09 16:40:44 +00:00
aceecfd14c Merge pull request 'Update LICENSE.md' (#34) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 6m44s
Reviewed-on: #34
2025-12-08 16:03:33 +00:00
1de7155771 Merge pull request 'fix: prevent race condition when channel is used before being set' (#33) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 3m46s
Reviewed-on: #33
2025-12-08 15:47:01 +00:00
79d77497a0 Merge pull request 'update: readme' (#32) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 6m22s
Reviewed-on: #32
2025-12-08 14:13:23 +00:00
20b90c1727 Merge pull request 'Update .gitea/workflows/build.yml' (#31) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 5m9s
Reviewed-on: #31
2025-12-08 13:37:07 +00:00
a1e920f6b5 Merge pull request 'refactor: remove magic numbers and replace with named constants' (#30) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 6m10s
Reviewed-on: bagas/tunnl_please#30
2025-12-08 12:46:56 +00:00
b5045409cb Merge pull request 'feat: add droping conn command' (#29) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 4m30s
Reviewed-on: bagas/tunnl_please#29
2025-12-07 09:30:47 +00:00
33e6ad08d7 Merge pull request 'staging' (#28) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 6m0s
Reviewed-on: bagas/tunnl_please#28
2025-12-06 16:47:20 +00:00
fd513d7bc9 Merge pull request 'refactor: instantiate new session object once forwarding is approved' (#27) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 6m2s
Reviewed-on: bagas/tunnl_please#27
2025-12-06 16:14:22 +00:00
73e7df6a3b Merge pull request 'fix: correct read/write handling in CustomWriter' (#26) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 5m14s
Reviewed-on: bagas/tunnl_please#26
2025-12-06 15:20:11 +00:00
d2c5b2a4db Merge pull request 'staging' (#25) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 4m21s
Reviewed-on: bagas/tunnl_please#25
2025-12-05 17:09:59 +00:00
cad22cd25a Merge pull request 'refactor: change to gitea action' (#21) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 4m19s
Reviewed-on: bagas/tunnl_please#21
2025-12-03 14:21:50 +00:00
0cb02f5220 Merge pull request 'refactor: remove profiling' (#20) from staging into main
Some checks failed
Docker Build and Push / build-and-push (push) Has been cancelled
Reviewed-on: bagas/tunnl_please#20
2025-12-03 14:20:52 +00:00
7bee2f2c9b Merge pull request 'staging' (#19) from staging into main
Some checks failed
Docker Build and Push / build-and-push (push) Failing after 2s
Reviewed-on: bagas/tunnl_please#19
2025-12-03 14:18:20 +00:00
54069ad305 Merge pull request 'fix: http forwarding stuck' (#9) from staging into main
Some checks failed
Docker Build and Push / build-and-push (push) Failing after 3s
Reviewed-on: bagas/tunnl_please#9
2025-11-28 12:36:00 +00:00
5bf618aa32 Merge pull request 'fix: logic error in port checking' (#8) from staging into main
Some checks failed
Docker Build and Push / build-and-push (push) Has been cancelled
Reviewed-on: bagas/tunnl_please#8
2025-11-28 10:34:28 +00:00
34041a9fe6 Merge pull request 'refactor: handle error' (#7) from staging into main
Some checks failed
Docker Build and Push / build-and-push (push) Has been cancelled
Reviewed-on: bagas/tunnl_please#7
2025-11-28 09:57:47 +00:00
c6d64aff3a Merge pull request 'refactor: separate session responsibilities and inject dependencies' (#6) from staging into main
Some checks failed
Docker Build and Push / build-and-push (push) Has been cancelled
Reviewed-on: bagas/tunnl_please#6
2025-11-28 06:30:09 +00:00
2e5a4e0b71 Merge pull request 'fix: close connection on TCP/IP request timeout' (#5) from staging into main
Some checks failed
Docker Build and Push / build-and-push (push) Has been cancelled
Reviewed-on: bagas/tunnl_please#5
2025-11-27 16:04:55 +00:00
36 changed files with 2092 additions and 947 deletions

32
.dockerignore Normal file
View File

@@ -0,0 +1,32 @@
.git/
.github/
.gitea/
.gitignore
.idea/
*.swp
*.swo
*~
tmp/
*.log
.env
.env.*
certs/
id_rsa*
*.pub
README.md
LICENSE.md
*.md
renovate.json
renovate-config.js
*_test.go
testdata/
app

View File

@@ -5,14 +5,25 @@ on:
branches: branches:
- main - main
- staging - staging
tags:
- 'v*'
paths:
- '**.go'
- 'go.mod'
- 'go.sum'
- 'Dockerfile'
- 'Dockerfile.*'
- '.dockerignore'
- '.gitea/workflows/build.yml'
jobs: jobs:
build-and-push: build-and-push-branches:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.ref_type == 'branch'
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -24,6 +35,17 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set version variables
id: vars
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "VERSION=dev-main" >> $GITHUB_OUTPUT
else
echo "VERSION=dev-staging" >> $GITHUB_OUTPUT
fi
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "COMMIT=${{ github.sha }}" >> $GITHUB_OUTPUT
- name: Build and push Docker image for main - name: Build and push Docker image for main
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -32,6 +54,10 @@ jobs:
tags: | tags: |
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:latest git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.vars.outputs.VERSION }}
BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }}
COMMIT=${{ steps.vars.outputs.COMMIT }}
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
- name: Build and push Docker image for staging - name: Build and push Docker image for staging
@@ -42,4 +68,85 @@ jobs:
tags: | tags: |
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:staging git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:staging
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
if: github.ref == 'refs/heads/staging' build-args: |
VERSION=${{ steps.vars.outputs.VERSION }}
BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }}
COMMIT=${{ steps.vars.outputs.COMMIT }}
if: github.ref == 'refs/heads/staging'
build-and-push-tags:
runs-on: ubuntu-latest
if: github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
registry: git.fossy.my.id
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract version and determine release type
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "COMMIT=${{ github.sha }}" >> $GITHUB_OUTPUT
if echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
echo "MAJOR=$MAJOR" >> $GITHUB_OUTPUT
echo "MINOR=$MINOR" >> $GITHUB_OUTPUT
if echo "$VERSION" | grep -q '-'; then
echo "IS_PRERELEASE=true" >> $GITHUB_OUTPUT
echo "ADDITIONAL_TAG=staging" >> $GITHUB_OUTPUT
else
echo "IS_PRERELEASE=false" >> $GITHUB_OUTPUT
echo "ADDITIONAL_TAG=latest" >> $GITHUB_OUTPUT
fi
else
echo "Invalid version format: $VERSION"
exit 1
fi
- name: Build and push Docker image for release
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.VERSION }}
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.MAJOR }}.${{ steps.version.outputs.MINOR }}
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.MAJOR }}
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:${{ steps.version.outputs.ADDITIONAL_TAG }}
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.version.outputs.VERSION }}
BUILD_DATE=${{ steps.version.outputs.BUILD_DATE }}
COMMIT=${{ steps.version.outputs.COMMIT }}
if: steps.version.outputs.IS_PRERELEASE == 'false'
- name: Build and push Docker image for pre-release
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.VERSION }}
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:${{ steps.version.outputs.ADDITIONAL_TAG }}
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.version.outputs.VERSION }}
BUILD_DATE=${{ steps.version.outputs.BUILD_DATE }}
COMMIT=${{ steps.version.outputs.COMMIT }}
if: steps.version.outputs.IS_PRERELEASE == 'true'

View File

@@ -0,0 +1,21 @@
name: renovate
on:
schedule:
- cron: "0 0 * * *"
push:
branches:
- staging
jobs:
renovate:
runs-on: ubuntu-latest
container: git.fossy.my.id/renovate-clanker/renovate:latest
steps:
- uses: actions/checkout@v6
- run: renovate
env:
RENOVATE_CONFIG_FILE: ${{ gitea.workspace }}/renovate-config.js
LOG_LEVEL: "debug"
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
GITHUB_COM_TOKEN: ${{ secrets.COM_TOKEN }}

View File

@@ -14,7 +14,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ id_rsa*
.env .env
tmp tmp
certs certs
app

View File

@@ -1,20 +1,59 @@
FROM golang:1.24.4-alpine AS go_builder FROM golang:1.25.5-alpine AS go_builder
ARG VERSION=dev
ARG BUILD_DATE=unknown
ARG COMMIT=unknown
RUN apk update && apk upgrade && \
apk add --no-cache ca-certificates tzdata git && \
update-ca-certificates
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download && go mod verify
COPY . . COPY . .
RUN apk update && apk upgrade && apk add --no-cache ca-certificates tzdata RUN --mount=type=cache,target=/go/pkg/mod \
RUN update-ca-certificates --mount=type=cache,target=/root/.cache/go-build \
RUN go build -o ./tmp/main CGO_ENABLED=0 GOOS=linux \
go build -trimpath \
-ldflags="-w -s -X tunnel_pls/version.Version=${VERSION} -X tunnel_pls/version.BuildDate=${BUILD_DATE} -X tunnel_pls/version.Commit=${COMMIT}" \
-o /app/tunnel_pls \
.
RUN adduser -D -u 10001 -g '' appuser && \
mkdir -p /app/certs/ssh /app/certs/tls && \
chown -R appuser:appuser /app
FROM scratch FROM scratch
WORKDIR /src ARG VERSION=dev
ARG BUILD_DATE=unknown
ARG COMMIT=unknown
COPY --from=go_builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=go_builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=go_builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=go_builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=go_builder /src/tmp/main /src COPY --from=go_builder /etc/passwd /etc/passwd
COPY --from=go_builder /etc/group /etc/group
COPY --from=go_builder --chown=appuser:appuser /app /app
ENV TZ Asia/Jakarta WORKDIR /app
ENTRYPOINT ["./main"] USER appuser
ENV TZ=Asia/Jakarta
EXPOSE 2200 8080 8443
LABEL org.opencontainers.image.title="Tunnel Please" \
org.opencontainers.image.description="SSH-based tunnel server" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${COMMIT}" \
org.opencontainers.image.created="${BUILD_DATE}"
ENTRYPOINT ["/app/tunnel_pls"]

121
README.md
View File

@@ -6,7 +6,6 @@ A lightweight SSH-based tunnel server written in Go that enables secure TCP and
- SSH interactive session with real-time command handling - SSH interactive session with real-time command handling
- Custom subdomain management for HTTP tunnels - Custom subdomain management for HTTP tunnels
- Active connection control with drop functionality
- Dual protocol support: HTTP and TCP tunnels - Dual protocol support: HTTP and TCP tunnels
- Real-time connection monitoring - Real-time connection monitoring
## Requirements ## Requirements
@@ -22,15 +21,13 @@ The following environment variables can be configured in the `.env` file:
|----------|-------------|---------|----------| |----------|-------------|---------|----------|
| `DOMAIN` | Domain name for subdomain routing | `localhost` | No | | `DOMAIN` | Domain name for subdomain routing | `localhost` | No |
| `PORT` | SSH server port | `2200` | No | | `PORT` | SSH server port | `2200` | No |
| `HTTP_PORT` | HTTP server port | `8080` | No |
| `HTTPS_PORT` | HTTPS server port | `8443` | No |
| `TLS_ENABLED` | Enable TLS/HTTPS | `false` | No | | `TLS_ENABLED` | Enable TLS/HTTPS | `false` | No |
| `TLS_REDIRECT` | Redirect HTTP to HTTPS | `false` | No | | `TLS_REDIRECT` | Redirect HTTP to HTTPS | `false` | No |
| `CERT_LOC` | Path to TLS certificate | `certs/cert.pem` | No |
| `KEY_LOC` | Path to TLS private key | `certs/privkey.pem` | No |
| `CERT_STORAGE_PATH` | Path for CertMagic certificate storage | `certs/certmagic` | No |
| `ACME_EMAIL` | Email for Let's Encrypt registration | `admin@<DOMAIN>` | No | | `ACME_EMAIL` | Email for Let's Encrypt registration | `admin@<DOMAIN>` | No |
| `CF_API_TOKEN` | Cloudflare API token for DNS-01 challenge | - | Yes (if auto-cert) | | `CF_API_TOKEN` | Cloudflare API token for DNS-01 challenge | - | Yes (if auto-cert) |
| `ACME_STAGING` | Use Let's Encrypt staging server | `false` | No | | `ACME_STAGING` | Use Let's Encrypt staging server | `false` | No |
| `SSH_PRIVATE_KEY` | Path to SSH private key (auto-generated if missing) | `certs/id_rsa` | No |
| `CORS_LIST` | Comma-separated list of allowed CORS origins | - | No | | `CORS_LIST` | Comma-separated list of allowed CORS origins | - | No |
| `ALLOWED_PORTS` | Port range for TCP tunnels (e.g., 40000-41000) | `40000-41000` | No | | `ALLOWED_PORTS` | Port range for TCP tunnels (e.g., 40000-41000) | `40000-41000` | No |
| `BUFFER_SIZE` | Buffer size for io.Copy operations in bytes (4096-1048576) | `32768` | No | | `BUFFER_SIZE` | Buffer size for io.Copy operations in bytes (4096-1048576) | `32768` | No |
@@ -43,8 +40,14 @@ The following environment variables can be configured in the `.env` file:
The server supports automatic TLS certificate generation and renewal using [CertMagic](https://github.com/caddyserver/certmagic) with Cloudflare DNS-01 challenge. This is required for wildcard certificate support (`*.yourdomain.com`). The server supports automatic TLS certificate generation and renewal using [CertMagic](https://github.com/caddyserver/certmagic) with Cloudflare DNS-01 challenge. This is required for wildcard certificate support (`*.yourdomain.com`).
**Certificate Storage:**
- TLS certificates are stored in `certs/tls/` (relative to application directory)
- User-provided certificates: `certs/tls/cert.pem` and `certs/tls/privkey.pem`
- CertMagic automatic certificates: `certs/tls/certmagic/`
- SSH keys are stored separately in `certs/ssh/`
**How it works:** **How it works:**
1. If user-provided certificates (`CERT_LOC`, `KEY_LOC`) exist and cover both `DOMAIN` and `*.DOMAIN`, they will be used 1. If user-provided certificates exist at `certs/tls/cert.pem` and `certs/tls/privkey.pem` and cover both `DOMAIN` and `*.DOMAIN`, they will be used
2. If certificates are missing, expired, expiring within 30 days, or don't cover the required domains, CertMagic will automatically obtain new certificates from Let's Encrypt 2. If certificates are missing, expired, expiring within 30 days, or don't cover the required domains, CertMagic will automatically obtain new certificates from Let's Encrypt
3. Certificates are automatically renewed before expiration 3. Certificates are automatically renewed before expiration
4. User-provided certificates support hot-reload (changes detected every 30 seconds) 4. User-provided certificates support hot-reload (changes detected every 30 seconds)
@@ -71,7 +74,7 @@ ACME_EMAIL=admin@example.com
### SSH Key Auto-Generation ### SSH Key Auto-Generation
If the SSH private key specified in `SSH_PRIVATE_KEY` doesn't exist, the application will automatically generate a new 4096-bit RSA key pair at the specified location. This makes it easier to get started without manually creating SSH keys. The application will automatically generate a new 4096-bit RSA key pair at `certs/ssh/id_rsa` if it doesn't exist. This makes it easier to get started without manually creating SSH keys. SSH keys are stored separately from TLS certificates.
### Memory Optimization ### Memory Optimization
@@ -112,6 +115,110 @@ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
go tool pprof http://localhost:6060/debug/pprof/heap go tool pprof http://localhost:6060/debug/pprof/heap
``` ```
## Docker Deployment
Three Docker Compose configurations are available for different deployment scenarios. Each configuration uses the image `git.fossy.my.id/bagas/tunnel-please:latest`.
### Configuration Options
#### 1. Root with Host Networking (RECOMMENDED)
**File:** `docker-compose.root.yml`
**Advantages:**
- Full TCP port forwarding support (ports 40000-41000)
- Direct binding to privileged ports (80, 443, 2200)
- Best performance with no NAT overhead
- Maximum flexibility for all tunnel types
- No port mapping limitations
**Use Case:** Production deployments where you need unrestricted TCP forwarding and maximum performance.
**Deploy:**
```bash
docker-compose -f docker-compose.root.yml up -d
```
#### 2. Standard (HTTP/HTTPS Only)
**File:** `docker-compose.standard.yml`
**Advantages:**
- Runs with unprivileged user (more secure)
- Standard port mappings (2200, 80, 443)
- Simple and predictable networking
- TCP port forwarding disabled (`ALLOWED_PORTS=none`)
**Use Case:** Deployments where you only need HTTP/HTTPS tunneling without custom TCP port forwarding.
**Deploy:**
```bash
docker-compose -f docker-compose.standard.yml up -d
```
#### 3. Limited TCP Forwarding
**File:** `docker-compose.tcp.yml`
**Advantages:**
- Runs with unprivileged user (more secure)
- Standard port mappings (2200, 80, 443)
- Limited TCP forwarding (ports 30000-31000)
- Controlled port range exposure
**Use Case:** Deployments where you need both HTTP/HTTPS tunneling and limited TCP forwarding within a specific port range.
**Deploy:**
```bash
docker-compose -f docker-compose.tcp.yml up -d
```
### Quick Start
1. **Choose your configuration** based on your requirements
2. **Edit the environment variables** in the chosen compose file:
- `DOMAIN`: Your domain name (e.g., `example.com`)
- `ACME_EMAIL`: Your email for Let's Encrypt
- `CF_API_TOKEN`: Your Cloudflare API token (if using automatic TLS)
3. **Deploy:**
```bash
docker-compose -f docker-compose.root.yml up -d
```
4. **Check logs:**
```bash
docker-compose -f docker-compose.root.yml logs -f
```
5. **Stop the service:**
```bash
docker-compose -f docker-compose.root.yml down
```
### Volume Management
All configurations use a named volume `certs` for persistent storage:
- SSH keys: `/app/certs/ssh/`
- TLS certificates: `/app/certs/tls/`
To backup certificates:
```bash
docker run --rm -v tunnel_pls_certs:/data -v $(pwd):/backup alpine tar czf /backup/certs-backup.tar.gz -C /data .
```
To restore certificates:
```bash
docker run --rm -v tunnel_pls_certs:/data -v $(pwd):/backup alpine tar xzf /backup/certs-backup.tar.gz -C /data
```
### Recommendation
**Use `docker-compose.root.yml`** for production deployments if you need:
- Full TCP port forwarding capabilities
- Any port range configuration
- Direct port binding without mapping overhead
- Maximum performance and flexibility
This is the recommended configuration for most use cases as it provides the complete feature set without limitations.
## Contributing ## Contributing
Contributions are welcome! Contributions are welcome!

37
docker-compose.root.yml Normal file
View File

@@ -0,0 +1,37 @@
version: '3.8'
services:
tunnel-please:
image: git.fossy.my.id/bagas/tunnel-please:latest
container_name: tunnel-please-root
user: root
network_mode: host
restart: unless-stopped
volumes:
- certs:/app/certs
environment:
DOMAIN: example.com
PORT: 2200
HTTP_PORT: 8080
HTTPS_PORT: 8443
TLS_ENABLED: "true"
TLS_REDIRECT: "true"
ACME_EMAIL: admin@example.com
CF_API_TOKEN: your_cloudflare_api_token_here
ACME_STAGING: "false"
CORS_LIST: http://localhost:3000,https://example.com
ALLOWED_PORTS: 40000-41000
BUFFER_SIZE: 32768
PPROF_ENABLED: "false"
PPROF_PORT: 6060
healthcheck:
test: ["CMD", "/bin/sh", "-c", "netstat -tln | grep -q :2200"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
volumes:
certs:
driver: local

View File

@@ -0,0 +1,39 @@
version: '3.8'
services:
tunnel-please:
image: git.fossy.my.id/bagas/tunnel-please:latest
container_name: tunnel-please-standard
restart: unless-stopped
ports:
- "2200:2200"
- "80:8080"
- "443:8443"
volumes:
- certs:/app/certs
environment:
DOMAIN: example.com
PORT: 2200
HTTP_PORT: 8080
HTTPS_PORT: 8443
TLS_ENABLED: "true"
TLS_REDIRECT: "true"
ACME_EMAIL: admin@example.com
CF_API_TOKEN: your_cloudflare_api_token_here
ACME_STAGING: "false"
CORS_LIST: http://localhost:3000,https://example.com
ALLOWED_PORTS: none
BUFFER_SIZE: 32768
PPROF_ENABLED: "false"
PPROF_PORT: 6060
healthcheck:
test: ["CMD", "/bin/sh", "-c", "netstat -tln | grep -q :2200"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
volumes:
certs:
driver: local

40
docker-compose.tcp.yml Normal file
View File

@@ -0,0 +1,40 @@
version: '3.8'
services:
tunnel-please:
image: git.fossy.my.id/bagas/tunnel-please:latest
container_name: tunnel-please-tcp
restart: unless-stopped
ports:
- "2200:2200"
- "80:8080"
- "443:8443"
- "30000-31000:30000-31000"
volumes:
- certs:/app/certs
environment:
DOMAIN: example.com
PORT: 2200
HTTP_PORT: 8080
HTTPS_PORT: 8443
TLS_ENABLED: "true"
TLS_REDIRECT: "true"
ACME_EMAIL: admin@example.com
CF_API_TOKEN: your_cloudflare_api_token_here
ACME_STAGING: "false"
CORS_LIST: http://localhost:3000,https://example.com
ALLOWED_PORTS: 30000-31000
BUFFER_SIZE: 32768
PPROF_ENABLED: "false"
PPROF_PORT: 6060
healthcheck:
test: ["CMD", "/bin/sh", "-c", "netstat -tln | grep -q :2200"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
volumes:
certs:
driver: local

36
go.mod
View File

@@ -3,26 +3,46 @@ module tunnel_pls
go 1.24.4 go 1.24.4
require ( require (
github.com/caddyserver/certmagic v0.25.0
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.45.0 github.com/libdns/cloudflare v0.2.2
golang.org/x/crypto v0.46.0
) )
require ( require (
github.com/caddyserver/certmagic v0.25.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/libdns/cloudflare v0.2.2 // indirect
github.com/libdns/libdns v1.1.1 // indirect github.com/libdns/libdns v1.1.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mholt/acmez/v3 v3.1.3 // indirect github.com/mholt/acmez/v3 v3.1.3 // indirect
github.com/miekg/dns v1.1.68 // indirect github.com/miekg/dns v1.1.68 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zeebo/blake3 v0.2.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/mod v0.29.0 // indirect golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.38.0 // indirect golang.org/x/tools v0.39.0 // indirect
) )

100
go.sum
View File

@@ -1,45 +1,107 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic= github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA= github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI= github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60= github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc= github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc=
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

35
internal/config/config.go Normal file
View File

@@ -0,0 +1,35 @@
package config
import (
"log"
"os"
"strconv"
"github.com/joho/godotenv"
)
func init() {
if _, err := os.Stat(".env"); err == nil {
if err := godotenv.Load(".env"); err != nil {
log.Printf("Warning: Failed to load .env file: %s", err)
}
}
}
func Getenv(key, defaultValue string) string {
val := os.Getenv(key)
if val == "" {
val = defaultValue
}
return val
}
func GetBufferSize() int {
sizeStr := Getenv("BUFFER_SIZE", "32768")
size, err := strconv.Atoi(sizeStr)
if err != nil || size < 4096 || size > 1048576 {
return 32768
}
return size
}

View File

@@ -1,4 +1,4 @@
package utils package key
import ( import (
"crypto/rand" "crypto/rand"
@@ -6,72 +6,12 @@ import (
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"log" "log"
mathrand "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/joho/godotenv"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type Env struct {
value map[string]string
mu sync.Mutex
}
var env *Env
func init() {
env = &Env{value: map[string]string{}}
}
func GenerateRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyz"
seededRand := mathrand.New(mathrand.NewSource(time.Now().UnixNano() + int64(mathrand.Intn(9999))))
var result strings.Builder
for i := 0; i < length; i++ {
randomIndex := seededRand.Intn(len(charset))
result.WriteString(string(charset[randomIndex]))
}
return result.String()
}
func Getenv(key, defaultValue string) string {
env.mu.Lock()
defer env.mu.Unlock()
if val, ok := env.value[key]; ok {
return val
}
if os.Getenv("HOSTNAME") == "" {
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading .env file: %s", err)
}
}
val := os.Getenv(key)
if val == "" {
val = defaultValue
}
env.value[key] = val
return val
}
func GetBufferSize() int {
sizeStr := Getenv("BUFFER_SIZE", "32768")
size, err := strconv.Atoi(sizeStr)
if err != nil || size < 4096 || size > 1048576 {
return 32768
}
return size
}
func GenerateSSHKeyIfNotExist(keyPath string) error { func GenerateSSHKeyIfNotExist(keyPath string) error {
if _, err := os.Stat(keyPath); err == nil { if _, err := os.Stat(keyPath); err == nil {
log.Printf("SSH key already exists at %s", keyPath) log.Printf("SSH key already exists at %s", keyPath)

View File

@@ -6,39 +6,50 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"tunnel_pls/utils" "tunnel_pls/internal/config"
) )
type PortManager struct { type Manager interface {
AddPortRange(startPort, endPort uint16) error
GetUnassignedPort() (uint16, bool)
SetPortStatus(port uint16, assigned bool) error
GetPortStatus(port uint16) (bool, bool)
}
type manager struct {
mu sync.RWMutex mu sync.RWMutex
ports map[uint16]bool ports map[uint16]bool
sortedPorts []uint16 sortedPorts []uint16
} }
var Manager = PortManager{ var Default Manager = &manager{
ports: make(map[uint16]bool), ports: make(map[uint16]bool),
sortedPorts: []uint16{}, sortedPorts: []uint16{},
} }
func init() { func init() {
rawRange := utils.Getenv("ALLOWED_PORTS", "40000-41000") rawRange := config.Getenv("ALLOWED_PORTS", "")
if rawRange == "" {
return
}
splitRange := strings.Split(rawRange, "-") splitRange := strings.Split(rawRange, "-")
if len(splitRange) != 2 { if len(splitRange) != 2 {
Manager.AddPortRange(30000, 31000) return
} else {
start, err := strconv.ParseUint(splitRange[0], 10, 16)
if err != nil {
start = 30000
}
end, err := strconv.ParseUint(splitRange[1], 10, 16)
if err != nil {
end = 31000
}
Manager.AddPortRange(uint16(start), uint16(end))
} }
start, err := strconv.ParseUint(splitRange[0], 10, 16)
if err != nil {
return
}
end, err := strconv.ParseUint(splitRange[1], 10, 16)
if err != nil {
return
}
_ = Default.AddPortRange(uint16(start), uint16(end))
} }
func (pm *PortManager) AddPortRange(startPort, endPort uint16) error { func (pm *manager) AddPortRange(startPort, endPort uint16) error {
pm.mu.Lock() pm.mu.Lock()
defer pm.mu.Unlock() defer pm.mu.Unlock()
@@ -57,7 +68,7 @@ func (pm *PortManager) AddPortRange(startPort, endPort uint16) error {
return nil return nil
} }
func (pm *PortManager) GetUnassignedPort() (uint16, bool) { func (pm *manager) GetUnassignedPort() (uint16, bool) {
pm.mu.Lock() pm.mu.Lock()
defer pm.mu.Unlock() defer pm.mu.Unlock()
@@ -70,7 +81,7 @@ func (pm *PortManager) GetUnassignedPort() (uint16, bool) {
return 0, false return 0, false
} }
func (pm *PortManager) SetPortStatus(port uint16, assigned bool) error { func (pm *manager) SetPortStatus(port uint16, assigned bool) error {
pm.mu.Lock() pm.mu.Lock()
defer pm.mu.Unlock() defer pm.mu.Unlock()
@@ -78,7 +89,7 @@ func (pm *PortManager) SetPortStatus(port uint16, assigned bool) error {
return nil return nil
} }
func (pm *PortManager) GetPortStatus(port uint16) (bool, bool) { func (pm *manager) GetPortStatus(port uint16) (bool, bool) {
pm.mu.RLock() pm.mu.RLock()
defer pm.mu.RUnlock() defer pm.mu.RUnlock()

18
internal/random/random.go Normal file
View File

@@ -0,0 +1,18 @@
package random
import (
mathrand "math/rand"
"strings"
"time"
)
func GenerateRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyz"
seededRand := mathrand.New(mathrand.NewSource(time.Now().UnixNano() + int64(mathrand.Intn(9999))))
var result strings.Builder
for i := 0; i < length; i++ {
randomIndex := seededRand.Intn(len(charset))
result.WriteString(string(charset[randomIndex]))
}
return result.String()
}

29
main.go
View File

@@ -6,19 +6,29 @@ import (
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
"tunnel_pls/internal/config"
"tunnel_pls/internal/key"
"tunnel_pls/server" "tunnel_pls/server"
"tunnel_pls/utils" "tunnel_pls/session"
"tunnel_pls/version"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
func main() { func main() {
if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-v") {
fmt.Println(version.GetVersion())
os.Exit(0)
}
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
log.SetFlags(log.LstdFlags | log.Lshortfile) log.SetFlags(log.LstdFlags | log.Lshortfile)
pprofEnabled := utils.Getenv("PPROF_ENABLED", "false") log.Printf("Starting %s", version.GetVersion())
pprofEnabled := config.Getenv("PPROF_ENABLED", "false")
if pprofEnabled == "true" { if pprofEnabled == "true" {
pprofPort := utils.Getenv("PPROF_PORT", "6060") pprofPort := config.Getenv("PPROF_PORT", "6060")
go func() { go func() {
pprofAddr := fmt.Sprintf("localhost:%s", pprofPort) pprofAddr := fmt.Sprintf("localhost:%s", pprofPort)
log.Printf("Starting pprof server on http://%s/debug/pprof/", pprofAddr) log.Printf("Starting pprof server on http://%s/debug/pprof/", pprofAddr)
@@ -30,11 +40,11 @@ func main() {
sshConfig := &ssh.ServerConfig{ sshConfig := &ssh.ServerConfig{
NoClientAuth: true, NoClientAuth: true,
ServerVersion: "SSH-2.0-TunnlPls-1.0", ServerVersion: fmt.Sprintf("SSH-2.0-TunnlPls-%s", version.GetShortVersion()),
} }
sshKeyPath := utils.Getenv("SSH_PRIVATE_KEY", "certs/id_rsa") sshKeyPath := "certs/ssh/id_rsa"
if err := utils.GenerateSSHKeyIfNotExist(sshKeyPath); err != nil { if err := key.GenerateSSHKeyIfNotExist(sshKeyPath); err != nil {
log.Fatalf("Failed to generate SSH key: %s", err) log.Fatalf("Failed to generate SSH key: %s", err)
} }
@@ -49,6 +59,11 @@ func main() {
} }
sshConfig.AddHostKey(private) sshConfig.AddHostKey(private)
app := server.NewServer(sshConfig) sessionRegistry := session.NewRegistry()
app, err := server.NewServer(sshConfig, sessionRegistry)
if err != nil {
log.Fatalf("Failed to start server: %s", err)
}
app.Start() app.Start()
} }

8
renovate-config.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
"endpoint": "https://git.fossy.my.id/api/v1",
"gitAuthor": "Renovate-Clanker <renovate-bot@fossy.my.id>",
"platform": "gitea",
"onboardingConfigFileName": "renovate.json",
"autodiscover": true,
"optimizeForDisabled": true,
};

19
renovate.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": [
"config:recommended"
],
"packageRules": [
{
"matchUpdateTypes": [
"minor",
"patch",
"pin",
"digest"
],
"automerge": true,
"baseBranchPatterns": [
"staging"
]
}
]
}

View File

@@ -1,28 +0,0 @@
package server
import (
"log"
"net"
"tunnel_pls/session"
"golang.org/x/crypto/ssh"
)
func (s *Server) handleConnection(conn net.Conn) {
sshConn, chans, forwardingReqs, err := ssh.NewServerConn(conn, s.Config)
if err != nil {
log.Printf("failed to establish SSH connection: %v", err)
err := conn.Close()
if err != nil {
log.Printf("failed to close SSH connection: %v", err)
return
}
return
}
log.Println("SSH connection established:", sshConn.User())
session.New(sshConn, forwardingReqs, chans)
return
}

View File

@@ -14,21 +14,38 @@ type HeaderManager interface {
Finalize() []byte Finalize() []byte
} }
type ResponseHeaderFactory struct { type ResponseHeaderManager interface {
Get(key string) string
Set(key string, value string)
Remove(key string)
Finalize() []byte
}
type RequestHeaderManager interface {
Get(key string) string
Set(key string, value string)
Remove(key string)
Finalize() []byte
GetMethod() string
GetPath() string
GetVersion() string
}
type responseHeaderFactory struct {
startLine []byte startLine []byte
headers map[string]string headers map[string]string
} }
type RequestHeaderFactory struct { type requestHeaderFactory struct {
Method string method string
Path string path string
Version string version string
startLine []byte startLine []byte
headers map[string]string headers map[string]string
} }
func NewRequestHeaderFactory(br *bufio.Reader) (*RequestHeaderFactory, error) { func NewRequestHeaderFactory(br *bufio.Reader) (RequestHeaderManager, error) {
header := &RequestHeaderFactory{ header := &requestHeaderFactory{
headers: make(map[string]string), headers: make(map[string]string),
} }
@@ -44,9 +61,9 @@ func NewRequestHeaderFactory(br *bufio.Reader) (*RequestHeaderFactory, error) {
return nil, fmt.Errorf("invalid request line") return nil, fmt.Errorf("invalid request line")
} }
header.Method = parts[0] header.method = parts[0]
header.Path = parts[1] header.path = parts[1]
header.Version = parts[2] header.version = parts[2]
for { for {
line, err := br.ReadString('\n') line, err := br.ReadString('\n')
@@ -69,8 +86,8 @@ func NewRequestHeaderFactory(br *bufio.Reader) (*RequestHeaderFactory, error) {
return header, nil return header, nil
} }
func NewResponseHeaderFactory(startLine []byte) *ResponseHeaderFactory { func NewResponseHeaderFactory(startLine []byte) ResponseHeaderManager {
header := &ResponseHeaderFactory{ header := &responseHeaderFactory{
startLine: nil, startLine: nil,
headers: make(map[string]string), headers: make(map[string]string),
} }
@@ -96,19 +113,19 @@ func NewResponseHeaderFactory(startLine []byte) *ResponseHeaderFactory {
return header return header
} }
func (resp *ResponseHeaderFactory) Get(key string) string { func (resp *responseHeaderFactory) Get(key string) string {
return resp.headers[key] return resp.headers[key]
} }
func (resp *ResponseHeaderFactory) Set(key string, value string) { func (resp *responseHeaderFactory) Set(key string, value string) {
resp.headers[key] = value resp.headers[key] = value
} }
func (resp *ResponseHeaderFactory) Remove(key string) { func (resp *responseHeaderFactory) Remove(key string) {
delete(resp.headers, key) delete(resp.headers, key)
} }
func (resp *ResponseHeaderFactory) Finalize() []byte { func (resp *responseHeaderFactory) Finalize() []byte {
var buf bytes.Buffer var buf bytes.Buffer
buf.Write(resp.startLine) buf.Write(resp.startLine)
@@ -125,7 +142,7 @@ func (resp *ResponseHeaderFactory) Finalize() []byte {
return buf.Bytes() return buf.Bytes()
} }
func (req *RequestHeaderFactory) Get(key string) string { func (req *requestHeaderFactory) Get(key string) string {
val, ok := req.headers[key] val, ok := req.headers[key]
if !ok { if !ok {
return "" return ""
@@ -133,15 +150,27 @@ func (req *RequestHeaderFactory) Get(key string) string {
return val return val
} }
func (req *RequestHeaderFactory) Set(key string, value string) { func (req *requestHeaderFactory) Set(key string, value string) {
req.headers[key] = value req.headers[key] = value
} }
func (req *RequestHeaderFactory) Remove(key string) { func (req *requestHeaderFactory) Remove(key string) {
delete(req.headers, key) delete(req.headers, key)
} }
func (req *RequestHeaderFactory) Finalize() []byte { func (req *requestHeaderFactory) GetMethod() string {
return req.method
}
func (req *requestHeaderFactory) GetPath() string {
return req.path
}
func (req *requestHeaderFactory) GetVersion() string {
return req.version
}
func (req *requestHeaderFactory) Finalize() []byte {
var buf bytes.Buffer var buf bytes.Buffer
buf.Write(req.startLine) buf.Write(req.startLine)

View File

@@ -11,34 +11,61 @@ import (
"regexp" "regexp"
"strings" "strings"
"time" "time"
"tunnel_pls/internal/config"
"tunnel_pls/session" "tunnel_pls/session"
"tunnel_pls/utils"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type Interaction interface { type HTTPWriter interface {
SendMessage(message string) io.Reader
} io.Writer
type CustomWriter struct { GetRemoteAddr() net.Addr
RemoteAddr net.Addr GetWriter() io.Writer
writer io.Writer AddResponseMiddleware(mw ResponseMiddleware)
reader io.Reader AddRequestStartMiddleware(mw RequestMiddleware)
headerBuf []byte SetRequestHeader(header RequestHeaderManager)
buf []byte GetRequestStartMiddleware() []RequestMiddleware
respHeader *ResponseHeaderFactory
reqHeader *RequestHeaderFactory
interaction Interaction
respMW []ResponseMiddleware
reqStartMW []RequestMiddleware
reqEndMW []RequestMiddleware
} }
func (cw *CustomWriter) SetInteraction(interaction Interaction) { type customWriter struct {
cw.interaction = interaction remoteAddr net.Addr
writer io.Writer
reader io.Reader
headerBuf []byte
buf []byte
respHeader ResponseHeaderManager
reqHeader RequestHeaderManager
respMW []ResponseMiddleware
reqStartMW []RequestMiddleware
reqEndMW []RequestMiddleware
} }
func (cw *CustomWriter) Read(p []byte) (int, error) { func (cw *customWriter) GetRemoteAddr() net.Addr {
return cw.remoteAddr
}
func (cw *customWriter) GetWriter() io.Writer {
return cw.writer
}
func (cw *customWriter) AddResponseMiddleware(mw ResponseMiddleware) {
cw.respMW = append(cw.respMW, mw)
}
func (cw *customWriter) AddRequestStartMiddleware(mw RequestMiddleware) {
cw.reqStartMW = append(cw.reqStartMW, mw)
}
func (cw *customWriter) SetRequestHeader(header RequestHeaderManager) {
cw.reqHeader = header
}
func (cw *customWriter) GetRequestStartMiddleware() []RequestMiddleware {
return cw.reqStartMW
}
func (cw *customWriter) Read(p []byte) (int, error) {
tmp := make([]byte, len(p)) tmp := make([]byte, len(p))
read, err := cw.reader.Read(tmp) read, err := cw.reader.Read(tmp)
if read == 0 && err != nil { if read == 0 && err != nil {
@@ -95,13 +122,12 @@ func (cw *CustomWriter) Read(p []byte) (int, error) {
return n, nil return n, nil
} }
func NewCustomWriter(writer io.Writer, reader io.Reader, remoteAddr net.Addr) *CustomWriter { func NewCustomWriter(writer io.Writer, reader io.Reader, remoteAddr net.Addr) HTTPWriter {
return &CustomWriter{ return &customWriter{
RemoteAddr: remoteAddr, remoteAddr: remoteAddr,
writer: writer, writer: writer,
reader: reader, reader: reader,
buf: make([]byte, 0, 4096), buf: make([]byte, 0, 4096),
interaction: nil,
} }
} }
@@ -129,7 +155,7 @@ func isHTTPHeader(buf []byte) bool {
return true return true
} }
func (cw *CustomWriter) Write(p []byte) (int, error) { func (cw *customWriter) Write(p []byte) (int, error) {
if cw.respHeader != nil && len(cw.buf) == 0 && len(p) >= 5 && string(p[0:5]) == "HTTP/" { if cw.respHeader != nil && len(cw.buf) == 0 && len(p) >= 5 && string(p[0:5]) == "HTTP/" {
cw.respHeader = nil cw.respHeader = nil
} }
@@ -186,18 +212,29 @@ func (cw *CustomWriter) Write(p []byte) (int, error) {
return len(p), nil return len(p), nil
} }
func (cw *CustomWriter) AddInteraction(interaction Interaction) {
cw.interaction = interaction
}
var redirectTLS = false var redirectTLS = false
func NewHTTPServer() error { type HTTPServer interface {
listener, err := net.Listen("tcp", ":80") ListenAndServe() error
ListenAndServeTLS() error
handler(conn net.Conn)
handlerTLS(conn net.Conn)
}
type httpServer struct {
sessionRegistry session.Registry
}
func NewHTTPServer(sessionRegistry session.Registry) HTTPServer {
return &httpServer{sessionRegistry: sessionRegistry}
}
func (hs *httpServer) ListenAndServe() error {
httpPort := config.Getenv("HTTP_PORT", "8080")
listener, err := net.Listen("tcp", ":"+httpPort)
if err != nil { if err != nil {
return errors.New("Error listening: " + err.Error()) return errors.New("Error listening: " + err.Error())
} }
if utils.Getenv("TLS_ENABLED", "false") == "true" && utils.Getenv("TLS_REDIRECT", "false") == "true" { if config.Getenv("TLS_ENABLED", "false") == "true" && config.Getenv("TLS_REDIRECT", "false") == "true" {
redirectTLS = true redirectTLS = true
} }
go func() { go func() {
@@ -212,13 +249,13 @@ func NewHTTPServer() error {
continue continue
} }
go Handler(conn) go hs.handler(conn)
} }
}() }()
return nil return nil
} }
func Handler(conn net.Conn) { func (hs *httpServer) handler(conn net.Conn) {
defer func() { defer func() {
err := conn.Close() err := conn.Close()
if err != nil && !errors.Is(err, net.ErrClosed) { if err != nil && !errors.Is(err, net.ErrClosed) {
@@ -249,7 +286,7 @@ func Handler(conn net.Conn) {
if redirectTLS { if redirectTLS {
_, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" + _, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
fmt.Sprintf("Location: https://%s.%s/\r\n", slug, utils.Getenv("DOMAIN", "localhost")) + fmt.Sprintf("Location: https://%s.%s/\r\n", slug, config.Getenv("DOMAIN", "localhost")) +
"Content-Length: 0\r\n" + "Content-Length: 0\r\n" +
"Connection: close\r\n" + "Connection: close\r\n" +
"\r\n")) "\r\n"))
@@ -277,8 +314,8 @@ func Handler(conn net.Conn) {
return return
} }
sshSession, ok := session.Clients[slug] sshSession, exist := hs.sessionRegistry.Get(slug)
if !ok { if !exist {
_, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" + _, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
fmt.Sprintf("Location: https://tunnl.live/tunnel-not-found?slug=%s\r\n", slug) + fmt.Sprintf("Location: https://tunnl.live/tunnel-not-found?slug=%s\r\n", slug) +
"Content-Length: 0\r\n" + "Content-Length: 0\r\n" +
@@ -291,13 +328,12 @@ func Handler(conn net.Conn) {
return return
} }
cw := NewCustomWriter(conn, dstReader, conn.RemoteAddr()) cw := NewCustomWriter(conn, dstReader, conn.RemoteAddr())
cw.SetInteraction(sshSession.Interaction)
forwardRequest(cw, reqhf, sshSession) forwardRequest(cw, reqhf, sshSession)
return return
} }
func forwardRequest(cw *CustomWriter, initialRequest *RequestHeaderFactory, sshSession *session.SSHSession) { func forwardRequest(cw HTTPWriter, initialRequest RequestHeaderManager, sshSession *session.SSHSession) {
payload := sshSession.Forwarder.CreateForwardedTCPIPPayload(cw.RemoteAddr) payload := sshSession.GetForwarder().CreateForwardedTCPIPPayload(cw.GetRemoteAddr())
type channelResult struct { type channelResult struct {
channel ssh.Channel channel ssh.Channel
@@ -307,7 +343,7 @@ func forwardRequest(cw *CustomWriter, initialRequest *RequestHeaderFactory, sshS
resultChan := make(chan channelResult, 1) resultChan := make(chan channelResult, 1)
go func() { go func() {
channel, reqs, err := sshSession.Lifecycle.GetConnection().OpenChannel("forwarded-tcpip", payload) channel, reqs, err := sshSession.GetLifecycle().GetConnection().OpenChannel("forwarded-tcpip", payload)
resultChan <- channelResult{channel, reqs, err} resultChan <- channelResult{channel, reqs, err}
}() }()
@@ -318,29 +354,28 @@ func forwardRequest(cw *CustomWriter, initialRequest *RequestHeaderFactory, sshS
case result := <-resultChan: case result := <-resultChan:
if result.err != nil { if result.err != nil {
log.Printf("Failed to open forwarded-tcpip channel: %v", result.err) log.Printf("Failed to open forwarded-tcpip channel: %v", result.err)
sshSession.Forwarder.WriteBadGatewayResponse(cw.writer) sshSession.GetForwarder().WriteBadGatewayResponse(cw.GetWriter())
return return
} }
channel = result.channel channel = result.channel
reqs = result.reqs reqs = result.reqs
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):
log.Printf("Timeout opening forwarded-tcpip channel") log.Printf("Timeout opening forwarded-tcpip channel")
sshSession.Forwarder.WriteBadGatewayResponse(cw.writer) sshSession.GetForwarder().WriteBadGatewayResponse(cw.GetWriter())
return return
} }
go ssh.DiscardRequests(reqs) go ssh.DiscardRequests(reqs)
fingerprintMiddleware := NewTunnelFingerprint() fingerprintMiddleware := NewTunnelFingerprint()
forwardedForMiddleware := NewForwardedFor(cw.RemoteAddr) forwardedForMiddleware := NewForwardedFor(cw.GetRemoteAddr())
cw.respMW = append(cw.respMW, fingerprintMiddleware) cw.AddResponseMiddleware(fingerprintMiddleware)
cw.reqStartMW = append(cw.reqStartMW, forwardedForMiddleware) cw.AddRequestStartMiddleware(forwardedForMiddleware)
cw.reqEndMW = nil cw.SetRequestHeader(initialRequest)
cw.reqHeader = initialRequest
for _, m := range cw.reqStartMW { for _, m := range cw.GetRequestStartMiddleware() {
if err := m.HandleRequest(cw.reqHeader); err != nil { if err := m.HandleRequest(initialRequest); err != nil {
log.Printf("Error handling request: %v", err) log.Printf("Error handling request: %v", err)
return return
} }
@@ -352,6 +387,6 @@ func forwardRequest(cw *CustomWriter, initialRequest *RequestHeaderFactory, sshS
return return
} }
sshSession.Forwarder.HandleConnection(cw, channel, cw.RemoteAddr) sshSession.GetForwarder().HandleConnection(cw, channel, cw.GetRemoteAddr())
return return
} }

View File

@@ -8,19 +8,19 @@ import (
"log" "log"
"net" "net"
"strings" "strings"
"tunnel_pls/session" "tunnel_pls/internal/config"
"tunnel_pls/utils"
) )
func NewHTTPSServer() error { func (hs *httpServer) ListenAndServeTLS() error {
domain := utils.Getenv("DOMAIN", "localhost") domain := config.Getenv("DOMAIN", "localhost")
httpsPort := config.Getenv("HTTPS_PORT", "8443")
tlsConfig, err := NewTLSConfig(domain) tlsConfig, err := NewTLSConfig(domain)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize TLS config: %w", err) return fmt.Errorf("failed to initialize TLS config: %w", err)
} }
ln, err := tls.Listen("tcp", ":443", tlsConfig) ln, err := tls.Listen("tcp", ":"+httpsPort, tlsConfig)
if err != nil { if err != nil {
return err return err
} }
@@ -37,13 +37,13 @@ func NewHTTPSServer() error {
continue continue
} }
go HandlerTLS(conn) go hs.handlerTLS(conn)
} }
}() }()
return nil return nil
} }
func HandlerTLS(conn net.Conn) { func (hs *httpServer) handlerTLS(conn net.Conn) {
defer func() { defer func() {
err := conn.Close() err := conn.Close()
if err != nil { if err != nil {
@@ -89,8 +89,8 @@ func HandlerTLS(conn net.Conn) {
return return
} }
sshSession, ok := session.Clients[slug] sshSession, exist := hs.sessionRegistry.Get(slug)
if !ok { if !exist {
_, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" + _, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
fmt.Sprintf("Location: https://tunnl.live/tunnel-not-found?slug=%s\r\n", slug) + fmt.Sprintf("Location: https://tunnl.live/tunnel-not-found?slug=%s\r\n", slug) +
"Content-Length: 0\r\n" + "Content-Length: 0\r\n" +
@@ -103,7 +103,6 @@ func HandlerTLS(conn net.Conn) {
return return
} }
cw := NewCustomWriter(conn, dstReader, conn.RemoteAddr()) cw := NewCustomWriter(conn, dstReader, conn.RemoteAddr())
cw.SetInteraction(sshSession.Interaction)
forwardRequest(cw, reqhf, sshSession) forwardRequest(cw, reqhf, sshSession)
return return
} }

View File

@@ -5,11 +5,11 @@ import (
) )
type RequestMiddleware interface { type RequestMiddleware interface {
HandleRequest(header *RequestHeaderFactory) error HandleRequest(header RequestHeaderManager) error
} }
type ResponseMiddleware interface { type ResponseMiddleware interface {
HandleResponse(header *ResponseHeaderFactory, body []byte) error HandleResponse(header ResponseHeaderManager, body []byte) error
} }
type TunnelFingerprint struct{} type TunnelFingerprint struct{}
@@ -18,16 +18,11 @@ func NewTunnelFingerprint() *TunnelFingerprint {
return &TunnelFingerprint{} return &TunnelFingerprint{}
} }
func (h *TunnelFingerprint) HandleResponse(header *ResponseHeaderFactory, body []byte) error { func (h *TunnelFingerprint) HandleResponse(header ResponseHeaderManager, body []byte) error {
header.Set("Server", "Tunnel Please") header.Set("Server", "Tunnel Please")
return nil return nil
} }
type RequestLogger struct {
interaction Interaction
remoteAddr net.Addr
}
type ForwardedFor struct { type ForwardedFor struct {
addr net.Addr addr net.Addr
} }
@@ -36,7 +31,7 @@ func NewForwardedFor(addr net.Addr) *ForwardedFor {
return &ForwardedFor{addr: addr} return &ForwardedFor{addr: addr}
} }
func (ff *ForwardedFor) HandleRequest(header *RequestHeaderFactory) error { func (ff *ForwardedFor) HandleRequest(header RequestHeaderManager) error {
host, _, err := net.SplitHostPort(ff.addr.String()) host, _, err := net.SplitHostPort(ff.addr.String())
if err != nil { if err != nil {
return err return err

View File

@@ -4,44 +4,51 @@ import (
"fmt" "fmt"
"log" "log"
"net" "net"
"net/http" "tunnel_pls/internal/config"
"tunnel_pls/utils" "tunnel_pls/session"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type Server struct { type Server struct {
Conn *net.Listener conn *net.Listener
Config *ssh.ServerConfig config *ssh.ServerConfig
HttpServer *http.Server sessionRegistry session.Registry
} }
func NewServer(config *ssh.ServerConfig) *Server { func NewServer(sshConfig *ssh.ServerConfig, sessionRegistry session.Registry) (*Server, error) {
listener, err := net.Listen("tcp", fmt.Sprintf(":%s", utils.Getenv("PORT", "2200"))) listener, err := net.Listen("tcp", fmt.Sprintf(":%s", config.Getenv("PORT", "2200")))
if err != nil { if err != nil {
log.Fatalf("failed to listen on port 2200: %v", err) log.Fatalf("failed to listen on port 2200: %v", err)
return nil return nil, err
} }
if utils.Getenv("TLS_ENABLED", "false") == "true" {
err = NewHTTPSServer() HttpServer := NewHTTPServer(sessionRegistry)
if err != nil { err = HttpServer.ListenAndServe()
log.Fatalf("failed to start https server: %v", err)
}
}
err = NewHTTPServer()
if err != nil { if err != nil {
log.Fatalf("failed to start http server: %v", err) log.Fatalf("failed to start http server: %v", err)
return nil, err
} }
if config.Getenv("TLS_ENABLED", "false") == "true" {
err = HttpServer.ListenAndServeTLS()
if err != nil {
log.Fatalf("failed to start https server: %v", err)
return nil, err
}
}
return &Server{ return &Server{
Conn: &listener, conn: &listener,
Config: config, config: sshConfig,
} sessionRegistry: sessionRegistry,
}, nil
} }
func (s *Server) Start() { func (s *Server) Start() {
log.Println("SSH server is starting on port 2200...") log.Println("SSH server is starting on port 2200...")
for { for {
conn, err := (*s.Conn).Accept() conn, err := (*s.conn).Accept()
if err != nil { if err != nil {
log.Printf("failed to accept connection: %v", err) log.Printf("failed to accept connection: %v", err)
continue continue
@@ -50,3 +57,26 @@ func (s *Server) Start() {
go s.handleConnection(conn) go s.handleConnection(conn)
} }
} }
func (s *Server) handleConnection(conn net.Conn) {
sshConn, chans, forwardingReqs, err := ssh.NewServerConn(conn, s.config)
if err != nil {
log.Printf("failed to establish SSH connection: %v", err)
err := conn.Close()
if err != nil {
log.Printf("failed to close SSH connection: %v", err)
return
}
return
}
log.Println("SSH connection established:", sshConn.User())
sshSession := session.New(sshConn, forwardingReqs, chans, s.sessionRegistry)
err = sshSession.Start()
if err != nil {
log.Printf("SSH session ended with error: %v", err)
return
}
return
}

View File

@@ -10,13 +10,22 @@ import (
"os" "os"
"sync" "sync"
"time" "time"
"tunnel_pls/utils" "tunnel_pls/internal/config"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/libdns/cloudflare" "github.com/libdns/cloudflare"
) )
type TLSManager struct { type TLSManager interface {
userCertsExistAndValid() bool
loadUserCerts() error
startCertWatcher()
initCertMagic() error
getTLSConfig() *tls.Config
getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
}
type tlsManager struct {
domain string domain string
certPath string certPath string
keyPath string keyPath string
@@ -30,18 +39,18 @@ type TLSManager struct {
useCertMagic bool useCertMagic bool
} }
var tlsManager *TLSManager var globalTLSManager TLSManager
var tlsManagerOnce sync.Once var tlsManagerOnce sync.Once
func NewTLSConfig(domain string) (*tls.Config, error) { func NewTLSConfig(domain string) (*tls.Config, error) {
var initErr error var initErr error
tlsManagerOnce.Do(func() { tlsManagerOnce.Do(func() {
certPath := utils.Getenv("CERT_LOC", "certs/cert.pem") certPath := "certs/tls/cert.pem"
keyPath := utils.Getenv("KEY_LOC", "certs/privkey.pem") keyPath := "certs/tls/privkey.pem"
storagePath := utils.Getenv("CERT_STORAGE_PATH", "certs/certmagic") storagePath := "certs/tls/certmagic"
tm := &TLSManager{ tm := &tlsManager{
domain: domain, domain: domain,
certPath: certPath, certPath: certPath,
keyPath: keyPath, keyPath: keyPath,
@@ -72,22 +81,22 @@ func NewTLSConfig(domain string) (*tls.Config, error) {
tm.useCertMagic = true tm.useCertMagic = true
} }
tlsManager = tm globalTLSManager = tm
}) })
if initErr != nil { if initErr != nil {
return nil, initErr return nil, initErr
} }
return tlsManager.getTLSConfig(), nil return globalTLSManager.getTLSConfig(), nil
} }
func isACMEConfigComplete() bool { func isACMEConfigComplete() bool {
cfAPIToken := utils.Getenv("CF_API_TOKEN", "") cfAPIToken := config.Getenv("CF_API_TOKEN", "")
return cfAPIToken != "" return cfAPIToken != ""
} }
func (tm *TLSManager) userCertsExistAndValid() bool { func (tm *tlsManager) userCertsExistAndValid() bool {
if _, err := os.Stat(tm.certPath); os.IsNotExist(err) { if _, err := os.Stat(tm.certPath); os.IsNotExist(err) {
log.Printf("Certificate file not found: %s", tm.certPath) log.Printf("Certificate file not found: %s", tm.certPath)
return false return false
@@ -158,7 +167,7 @@ func ValidateCertDomains(certPath, domain string) bool {
return hasBase && hasWildcard return hasBase && hasWildcard
} }
func (tm *TLSManager) loadUserCerts() error { func (tm *tlsManager) loadUserCerts() error {
cert, err := tls.LoadX509KeyPair(tm.certPath, tm.keyPath) cert, err := tls.LoadX509KeyPair(tm.certPath, tm.keyPath)
if err != nil { if err != nil {
return err return err
@@ -172,7 +181,7 @@ func (tm *TLSManager) loadUserCerts() error {
return nil return nil
} }
func (tm *TLSManager) startCertWatcher() { func (tm *tlsManager) startCertWatcher() {
go func() { go func() {
var lastCertMod, lastKeyMod time.Time var lastCertMod, lastKeyMod time.Time
@@ -227,14 +236,14 @@ func (tm *TLSManager) startCertWatcher() {
}() }()
} }
func (tm *TLSManager) initCertMagic() error { func (tm *tlsManager) initCertMagic() error {
if err := os.MkdirAll(tm.storagePath, 0700); err != nil { if err := os.MkdirAll(tm.storagePath, 0700); err != nil {
return fmt.Errorf("failed to create cert storage directory: %w", err) return fmt.Errorf("failed to create cert storage directory: %w", err)
} }
acmeEmail := utils.Getenv("ACME_EMAIL", "admin@"+tm.domain) acmeEmail := config.Getenv("ACME_EMAIL", "admin@"+tm.domain)
cfAPIToken := utils.Getenv("CF_API_TOKEN", "") cfAPIToken := config.Getenv("CF_API_TOKEN", "")
acmeStaging := utils.Getenv("ACME_STAGING", "false") == "true" acmeStaging := config.Getenv("ACME_STAGING", "false") == "true"
if cfAPIToken == "" { if cfAPIToken == "" {
return fmt.Errorf("CF_API_TOKEN environment variable is required for automatic certificate generation") return fmt.Errorf("CF_API_TOKEN environment variable is required for automatic certificate generation")
@@ -289,14 +298,14 @@ func (tm *TLSManager) initCertMagic() error {
return nil return nil
} }
func (tm *TLSManager) getTLSConfig() *tls.Config { func (tm *tlsManager) getTLSConfig() *tls.Config {
return &tls.Config{ return &tls.Config{
GetCertificate: tm.getCertificate, GetCertificate: tm.getCertificate,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
} }
} }
func (tm *TLSManager) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { func (tm *tlsManager) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if tm.useCertMagic { if tm.useCertMagic {
return tm.magic.GetCertificate(hello) return tm.magic.GetCertificate(hello)
} }

View File

@@ -10,16 +10,16 @@ import (
"strconv" "strconv"
"sync" "sync"
"time" "time"
"tunnel_pls/internal/config"
"tunnel_pls/session/slug" "tunnel_pls/session/slug"
"tunnel_pls/types" "tunnel_pls/types"
"tunnel_pls/utils"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
var bufferPool = sync.Pool{ var bufferPool = sync.Pool{
New: func() interface{} { New: func() interface{} {
bufSize := utils.GetBufferSize() bufSize := config.GetBufferSize()
return make([]byte, bufSize) return make([]byte, bufSize)
}, },
} }
@@ -31,11 +31,21 @@ func copyWithBuffer(dst io.Writer, src io.Reader) (written int64, err error) {
} }
type Forwarder struct { type Forwarder struct {
Listener net.Listener listener net.Listener
TunnelType types.TunnelType tunnelType types.TunnelType
ForwardedPort uint16 forwardedPort uint16
SlugManager slug.Manager slugManager slug.Manager
Lifecycle Lifecycle lifecycle Lifecycle
}
func NewForwarder(slugManager slug.Manager) *Forwarder {
return &Forwarder{
listener: nil,
tunnelType: "",
forwardedPort: 0,
slugManager: slugManager,
lifecycle: nil,
}
} }
type Lifecycle interface { type Lifecycle interface {
@@ -58,7 +68,7 @@ type ForwardingController interface {
} }
func (f *Forwarder) SetLifecycle(lifecycle Lifecycle) { func (f *Forwarder) SetLifecycle(lifecycle Lifecycle) {
f.Lifecycle = lifecycle f.lifecycle = lifecycle
} }
func (f *Forwarder) AcceptTCPConnections() { func (f *Forwarder) AcceptTCPConnections() {
@@ -90,7 +100,7 @@ func (f *Forwarder) AcceptTCPConnections() {
resultChan := make(chan channelResult, 1) resultChan := make(chan channelResult, 1)
go func() { go func() {
channel, reqs, err := f.Lifecycle.GetConnection().OpenChannel("forwarded-tcpip", payload) channel, reqs, err := f.lifecycle.GetConnection().OpenChannel("forwarded-tcpip", payload)
resultChan <- channelResult{channel, reqs, err} resultChan <- channelResult{channel, reqs, err}
}() }()
@@ -164,27 +174,27 @@ func (f *Forwarder) HandleConnection(dst io.ReadWriter, src ssh.Channel, remoteA
} }
func (f *Forwarder) SetType(tunnelType types.TunnelType) { func (f *Forwarder) SetType(tunnelType types.TunnelType) {
f.TunnelType = tunnelType f.tunnelType = tunnelType
} }
func (f *Forwarder) GetTunnelType() types.TunnelType { func (f *Forwarder) GetTunnelType() types.TunnelType {
return f.TunnelType return f.tunnelType
} }
func (f *Forwarder) GetForwardedPort() uint16 { func (f *Forwarder) GetForwardedPort() uint16 {
return f.ForwardedPort return f.forwardedPort
} }
func (f *Forwarder) SetForwardedPort(port uint16) { func (f *Forwarder) SetForwardedPort(port uint16) {
f.ForwardedPort = port f.forwardedPort = port
} }
func (f *Forwarder) SetListener(listener net.Listener) { func (f *Forwarder) SetListener(listener net.Listener) {
f.Listener = listener f.listener = listener
} }
func (f *Forwarder) GetListener() net.Listener { func (f *Forwarder) GetListener() net.Listener {
return f.Listener return f.listener
} }
func (f *Forwarder) WriteBadGatewayResponse(dst io.Writer) { func (f *Forwarder) WriteBadGatewayResponse(dst io.Writer) {
@@ -197,7 +207,7 @@ func (f *Forwarder) WriteBadGatewayResponse(dst io.Writer) {
func (f *Forwarder) Close() error { func (f *Forwarder) Close() error {
if f.GetListener() != nil { if f.GetListener() != nil {
return f.Listener.Close() return f.listener.Close()
} }
return nil return nil
} }

View File

@@ -7,10 +7,9 @@ import (
"log" "log"
"net" "net"
portUtil "tunnel_pls/internal/port" portUtil "tunnel_pls/internal/port"
"tunnel_pls/internal/random"
"tunnel_pls/types" "tunnel_pls/types"
"tunnel_pls/utils"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@@ -19,7 +18,28 @@ var blockedReservedPorts = []uint16{1080, 1433, 1521, 1900, 2049, 3306, 3389, 54
func (s *SSHSession) HandleGlobalRequest(GlobalRequest <-chan *ssh.Request) { func (s *SSHSession) HandleGlobalRequest(GlobalRequest <-chan *ssh.Request) {
for req := range GlobalRequest { for req := range GlobalRequest {
switch req.Type { switch req.Type {
case "shell", "pty-req", "window-change": case "shell", "pty-req":
err := req.Reply(true, nil)
if err != nil {
log.Println("Failed to reply to request:", err)
return
}
case "window-change":
p := req.Payload
if len(p) < 16 {
log.Println("invalid window-change payload")
err := req.Reply(false, nil)
if err != nil {
log.Println("Failed to reply to request:", err)
return
}
return
}
cols := binary.BigEndian.Uint32(p[0:4])
rows := binary.BigEndian.Uint32(p[4:8])
s.interaction.SetWH(int(cols), int(rows))
err := req.Reply(true, nil) err := req.Reply(true, nil)
if err != nil { if err != nil {
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
@@ -49,7 +69,7 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
return return
} }
err = s.Lifecycle.Close() err = s.lifecycle.Close()
if err != nil { if err != nil {
log.Printf("failed to close session: %v", err) log.Printf("failed to close session: %v", err)
} }
@@ -59,13 +79,12 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
var rawPortToBind uint32 var rawPortToBind uint32
if err := binary.Read(reader, binary.BigEndian, &rawPortToBind); err != nil { if err := binary.Read(reader, binary.BigEndian, &rawPortToBind); err != nil {
log.Println("Failed to read port from payload:", err) log.Println("Failed to read port from payload:", err)
s.Interaction.SendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port. (02) \r\n", rawPortToBind))
err := req.Reply(false, nil) err := req.Reply(false, nil)
if err != nil { if err != nil {
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
return return
} }
err = s.Lifecycle.Close() err = s.lifecycle.Close()
if err != nil { if err != nil {
log.Printf("failed to close session: %v", err) log.Printf("failed to close session: %v", err)
} }
@@ -73,13 +92,13 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
} }
if rawPortToBind > 65535 { if rawPortToBind > 65535 {
s.Interaction.SendMessage(fmt.Sprintf("Port %d is larger then allowed port of 65535. (02)\r\n", rawPortToBind)) log.Printf("Port %d is larger than allowed port of 65535", rawPortToBind)
err := req.Reply(false, nil) err := req.Reply(false, nil)
if err != nil { if err != nil {
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
return return
} }
err = s.Lifecycle.Close() err = s.lifecycle.Close()
if err != nil { if err != nil {
log.Printf("failed to close session: %v", err) log.Printf("failed to close session: %v", err)
} }
@@ -87,15 +106,14 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
} }
portToBind := uint16(rawPortToBind) portToBind := uint16(rawPortToBind)
if isBlockedPort(portToBind) { if isBlockedPort(portToBind) {
s.Interaction.SendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port. (02)\r\n", portToBind)) log.Printf("Port %d is blocked or restricted", portToBind)
err := req.Reply(false, nil) err := req.Reply(false, nil)
if err != nil { if err != nil {
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
return return
} }
err = s.Lifecycle.Close() err = s.lifecycle.Close()
if err != nil { if err != nil {
log.Printf("failed to close session: %v", err) log.Printf("failed to close session: %v", err)
} }
@@ -105,56 +123,49 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
if portToBind == 80 || portToBind == 443 { if portToBind == 80 || portToBind == 443 {
s.HandleHTTPForward(req, portToBind) s.HandleHTTPForward(req, portToBind)
return return
} else { }
if portToBind == 0 { if portToBind == 0 {
unassign, success := portUtil.Manager.GetUnassignedPort() unassign, success := portUtil.Default.GetUnassignedPort()
portToBind = unassign portToBind = unassign
if !success { if !success {
s.Interaction.SendMessage("No available port\r\n") log.Println("No available port")
err := req.Reply(false, nil)
if err != nil {
log.Println("Failed to reply to request:", err)
return
}
err = s.Lifecycle.Close()
if err != nil {
log.Printf("failed to close session: %v", err)
}
return
}
} else if isUse, isExist := portUtil.Manager.GetPortStatus(portToBind); isExist && isUse {
s.Interaction.SendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port. (03)\r\n", portToBind))
err := req.Reply(false, nil) err := req.Reply(false, nil)
if err != nil { if err != nil {
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
return return
} }
err = s.Lifecycle.Close() err = s.lifecycle.Close()
if err != nil { if err != nil {
log.Printf("failed to close session: %v", err) log.Printf("failed to close session: %v", err)
} }
return return
} }
err := portUtil.Manager.SetPortStatus(portToBind, true) } else if isUse, isExist := portUtil.Default.GetPortStatus(portToBind); isExist && isUse {
log.Printf("Port %d is already in use or restricted", portToBind)
err := req.Reply(false, nil)
if err != nil { if err != nil {
log.Println("Failed to set port status:", err) log.Println("Failed to reply to request:", err)
return return
} }
err = s.lifecycle.Close()
if err != nil {
log.Printf("failed to close session: %v", err)
}
return
} }
err = portUtil.Default.SetPortStatus(portToBind, true)
if err != nil {
log.Println("Failed to set port status:", err)
return
}
s.HandleTCPForward(req, addr, portToBind) s.HandleTCPForward(req, addr, portToBind)
} }
func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) { func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
slug := generateUniqueSlug() slug := random.GenerateRandomString(20)
if slug == "" {
err := req.Reply(false, nil)
if err != nil {
log.Println("Failed to reply to request:", err)
}
return
}
if !registerClient(slug, s) { if !s.registry.Register(slug, s) {
log.Printf("Failed to register client with slug: %s", slug) log.Printf("Failed to register client with slug: %s", slug)
err := req.Reply(false, nil) err := req.Reply(false, nil)
if err != nil { if err != nil {
@@ -167,7 +178,7 @@ func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
err := binary.Write(buf, binary.BigEndian, uint32(portToBind)) err := binary.Write(buf, binary.BigEndian, uint32(portToBind))
if err != nil { if err != nil {
log.Println("Failed to write port to buffer:", err) log.Println("Failed to write port to buffer:", err)
unregisterClient(slug) s.registry.Remove(slug)
err = req.Reply(false, nil) err = req.Reply(false, nil)
if err != nil { if err != nil {
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
@@ -176,16 +187,10 @@ func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
} }
log.Printf("HTTP forwarding approved on port: %d", portToBind) log.Printf("HTTP forwarding approved on port: %d", portToBind)
domain := utils.Getenv("DOMAIN", "localhost")
protocol := "http"
if utils.Getenv("TLS_ENABLED", "false") == "true" {
protocol = "https"
}
err = req.Reply(true, buf.Bytes()) err = req.Reply(true, buf.Bytes())
if err != nil { if err != nil {
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
unregisterClient(slug) s.registry.Remove(slug)
err = req.Reply(false, nil) err = req.Reply(false, nil)
if err != nil { if err != nil {
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
@@ -193,22 +198,19 @@ func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
return return
} }
s.Forwarder.SetType(types.HTTP) s.forwarder.SetType(types.HTTP)
s.Forwarder.SetForwardedPort(portToBind) s.forwarder.SetForwardedPort(portToBind)
s.SlugManager.Set(slug) s.slugManager.Set(slug)
s.Interaction.SendMessage("\033[H\033[2J") s.lifecycle.SetStatus(types.RUNNING)
s.Interaction.ShowWelcomeMessage() s.interaction.Start()
s.Interaction.SendMessage(fmt.Sprintf("Forwarding your traffic to %s://%s.%s\r\n", protocol, slug, domain))
s.Lifecycle.SetStatus(types.RUNNING)
s.Interaction.HandleUserInput()
} }
func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind uint16) { func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind uint16) {
log.Printf("Requested forwarding on %s:%d", addr, portToBind) log.Printf("Requested forwarding on %s:%d", addr, portToBind)
listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", portToBind)) listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", portToBind))
if err != nil { if err != nil {
s.Interaction.SendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port.\r\n", portToBind)) log.Printf("Port %d is already in use or restricted", portToBind)
if setErr := portUtil.Manager.SetPortStatus(portToBind, false); setErr != nil { if setErr := portUtil.Default.SetPortStatus(portToBind, false); setErr != nil {
log.Printf("Failed to reset port status: %v", setErr) log.Printf("Failed to reset port status: %v", setErr)
} }
err = req.Reply(false, nil) err = req.Reply(false, nil)
@@ -216,7 +218,7 @@ func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
return return
} }
err = s.Lifecycle.Close() err = s.lifecycle.Close()
if err != nil { if err != nil {
log.Printf("failed to close session: %v", err) log.Printf("failed to close session: %v", err)
} }
@@ -227,7 +229,7 @@ func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind
err = binary.Write(buf, binary.BigEndian, uint32(portToBind)) err = binary.Write(buf, binary.BigEndian, uint32(portToBind))
if err != nil { if err != nil {
log.Println("Failed to write port to buffer:", err) log.Println("Failed to write port to buffer:", err)
if setErr := portUtil.Manager.SetPortStatus(portToBind, false); setErr != nil { if setErr := portUtil.Default.SetPortStatus(portToBind, false); setErr != nil {
log.Printf("Failed to reset port status: %v", setErr) log.Printf("Failed to reset port status: %v", setErr)
} }
err = listener.Close() err = listener.Close()
@@ -242,7 +244,7 @@ func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind
err = req.Reply(true, buf.Bytes()) err = req.Reply(true, buf.Bytes())
if err != nil { if err != nil {
log.Println("Failed to reply to request:", err) log.Println("Failed to reply to request:", err)
if setErr := portUtil.Manager.SetPortStatus(portToBind, false); setErr != nil { if setErr := portUtil.Default.SetPortStatus(portToBind, false); setErr != nil {
log.Printf("Failed to reset port status: %v", setErr) log.Printf("Failed to reset port status: %v", setErr)
} }
err = listener.Close() err = listener.Close()
@@ -253,34 +255,12 @@ func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind
return return
} }
s.Forwarder.SetType(types.TCP) s.forwarder.SetType(types.TCP)
s.Forwarder.SetListener(listener) s.forwarder.SetListener(listener)
s.Forwarder.SetForwardedPort(portToBind) s.forwarder.SetForwardedPort(portToBind)
s.Interaction.SendMessage("\033[H\033[2J") s.lifecycle.SetStatus(types.RUNNING)
s.Interaction.ShowWelcomeMessage() go s.forwarder.AcceptTCPConnections()
s.Interaction.SendMessage(fmt.Sprintf("Forwarding your traffic to tcp://%s:%d \r\n", utils.Getenv("DOMAIN", "localhost"), s.Forwarder.GetForwardedPort())) s.interaction.Start()
s.Lifecycle.SetStatus(types.RUNNING)
go s.Forwarder.AcceptTCPConnections()
s.Interaction.HandleUserInput()
}
func generateUniqueSlug() string {
maxAttempts := 5
for i := 0; i < maxAttempts; i++ {
slug := utils.GenerateRandomString(20)
clientsMutex.RLock()
_, exists := Clients[slug]
clientsMutex.RUnlock()
if !exists {
return slug
}
}
log.Println("Failed to generate unique slug after multiple attempts")
return ""
} }
func readSSHString(reader *bytes.Reader) (string, error) { func readSSHString(reader *bytes.Reader) (string, error) {

View File

@@ -22,6 +22,131 @@ const (
paddingRight = 4 paddingRight = 4
) )
var forbiddenSlugs = []string{ var forbiddenSlugs = map[string]struct{}{
"ping", "ping": {},
"staging": {},
"admin": {},
"root": {},
"api": {},
"www": {},
"support": {},
"help": {},
"status": {},
"health": {},
"login": {},
"logout": {},
"signup": {},
"register": {},
"settings": {},
"config": {},
"null": {},
"undefined": {},
"example": {},
"test": {},
"dev": {},
"system": {},
"administrator": {},
"dashboard": {},
"account": {},
"profile": {},
"user": {},
"users": {},
"auth": {},
"oauth": {},
"callback": {},
"webhook": {},
"webhooks": {},
"static": {},
"assets": {},
"cdn": {},
"mail": {},
"email": {},
"ftp": {},
"ssh": {},
"git": {},
"svn": {},
"blog": {},
"news": {},
"about": {},
"contact": {},
"terms": {},
"privacy": {},
"legal": {},
"billing": {},
"payment": {},
"checkout": {},
"cart": {},
"shop": {},
"store": {},
"download": {},
"uploads": {},
"images": {},
"img": {},
"css": {},
"js": {},
"fonts": {},
"public": {},
"private": {},
"internal": {},
"external": {},
"proxy": {},
"cache": {},
"debug": {},
"metrics": {},
"monitoring": {},
"graphql": {},
"rest": {},
"rpc": {},
"socket": {},
"ws": {},
"wss": {},
"app": {},
"apps": {},
"mobile": {},
"desktop": {},
"embed": {},
"widget": {},
"docs": {},
"documentation": {},
"wiki": {},
"forum": {},
"community": {},
"feedback": {},
"report": {},
"abuse": {},
"spam": {},
"security": {},
"verify": {},
"confirm": {},
"reset": {},
"password": {},
"recovery": {},
"unsubscribe": {},
"subscribe": {},
"notifications": {},
"alerts": {},
"messages": {},
"inbox": {},
"outbox": {},
"sent": {},
"draft": {},
"trash": {},
"archive": {},
"search": {},
"explore": {},
"discover": {},
"trending": {},
"popular": {},
"featured": {},
"new": {},
"latest": {},
"top": {},
"best": {},
"hot": {},
"random": {},
"all": {},
"any": {},
"none": {},
"true": {},
"false": {},
} }

File diff suppressed because it is too large Load Diff

View File

@@ -11,10 +11,6 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type Interaction interface {
SendMessage(string)
}
type Forwarder interface { type Forwarder interface {
Close() error Close() error
GetTunnelType() types.TunnelType GetTunnelType() types.TunnelType
@@ -22,16 +18,25 @@ type Forwarder interface {
} }
type Lifecycle struct { type Lifecycle struct {
Status types.Status status types.Status
Conn ssh.Conn conn ssh.Conn
Channel ssh.Channel channel ssh.Channel
forwarder Forwarder
Interaction Interaction slugManager slug.Manager
Forwarder Forwarder
SlugManager slug.Manager
unregisterClient func(slug string) unregisterClient func(slug string)
} }
func NewLifecycle(conn ssh.Conn, forwarder Forwarder, slugManager slug.Manager) *Lifecycle {
return &Lifecycle{
status: "",
conn: conn,
channel: nil,
forwarder: forwarder,
slugManager: slugManager,
unregisterClient: nil,
}
}
func (l *Lifecycle) SetUnregisterClient(unregisterClient func(slug string)) { func (l *Lifecycle) SetUnregisterClient(unregisterClient func(slug string)) {
l.unregisterClient = unregisterClient l.unregisterClient = unregisterClient
} }
@@ -46,46 +51,46 @@ type SessionLifecycle interface {
} }
func (l *Lifecycle) GetChannel() ssh.Channel { func (l *Lifecycle) GetChannel() ssh.Channel {
return l.Channel return l.channel
} }
func (l *Lifecycle) SetChannel(channel ssh.Channel) { func (l *Lifecycle) SetChannel(channel ssh.Channel) {
l.Channel = channel l.channel = channel
} }
func (l *Lifecycle) GetConnection() ssh.Conn { func (l *Lifecycle) GetConnection() ssh.Conn {
return l.Conn return l.conn
} }
func (l *Lifecycle) SetStatus(status types.Status) { func (l *Lifecycle) SetStatus(status types.Status) {
l.Status = status l.status = status
} }
func (l *Lifecycle) Close() error { func (l *Lifecycle) Close() error {
err := l.Forwarder.Close() err := l.forwarder.Close()
if err != nil && !errors.Is(err, net.ErrClosed) { if err != nil && !errors.Is(err, net.ErrClosed) {
return err return err
} }
if l.Channel != nil { if l.channel != nil {
err := l.Channel.Close() err := l.channel.Close()
if err != nil && !errors.Is(err, io.EOF) { if err != nil && !errors.Is(err, io.EOF) {
return err return err
} }
} }
if l.Conn != nil { if l.conn != nil {
err := l.Conn.Close() err := l.conn.Close()
if err != nil && !errors.Is(err, net.ErrClosed) { if err != nil && !errors.Is(err, net.ErrClosed) {
return err return err
} }
} }
clientSlug := l.SlugManager.Get() clientSlug := l.slugManager.Get()
if clientSlug != "" { if clientSlug != "" {
l.unregisterClient(clientSlug) l.unregisterClient(clientSlug)
} }
if l.Forwarder.GetTunnelType() == types.TCP { if l.forwarder.GetTunnelType() == types.TCP {
err := portUtil.Manager.SetPortStatus(l.Forwarder.GetForwardedPort(), false) err := portUtil.Default.SetPortStatus(l.forwarder.GetForwardedPort(), false)
if err != nil { if err != nil {
return err return err
} }

66
session/registry.go Normal file
View File

@@ -0,0 +1,66 @@
package session
import "sync"
type Registry interface {
Get(slug string) (session *SSHSession, exist bool)
Update(oldSlug, newSlug string) (success bool)
Register(slug string, session *SSHSession) (success bool)
Remove(slug string)
}
type registry struct {
mu sync.RWMutex
clients map[string]*SSHSession
}
func NewRegistry() Registry {
return &registry{
clients: make(map[string]*SSHSession),
}
}
func (r *registry) Get(slug string) (session *SSHSession, exist bool) {
r.mu.RLock()
defer r.mu.RUnlock()
session, exist = r.clients[slug]
return
}
func (r *registry) Update(oldSlug, newSlug string) (success bool) {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.clients[newSlug]; exists && newSlug != oldSlug {
return false
}
client, ok := r.clients[oldSlug]
if !ok {
return false
}
delete(r.clients, oldSlug)
client.slugManager.Set(newSlug)
r.clients[newSlug] = client
return true
}
func (r *registry) Register(slug string, session *SSHSession) (success bool) {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.clients[slug]; exists {
return false
}
r.clients[slug] = session
return true
}
func (r *registry) Remove(slug string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.clients, slug)
}

View File

@@ -1,25 +1,18 @@
package session package session
import ( import (
"bytes"
"fmt" "fmt"
"log" "log"
"sync"
"time" "time"
"tunnel_pls/internal/config"
"tunnel_pls/session/forwarder" "tunnel_pls/session/forwarder"
"tunnel_pls/session/interaction" "tunnel_pls/session/interaction"
"tunnel_pls/session/lifecycle" "tunnel_pls/session/lifecycle"
"tunnel_pls/session/slug" "tunnel_pls/session/slug"
"tunnel_pls/utils"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
var (
clientsMutex sync.RWMutex
Clients = make(map[string]*SSHSession)
)
type Session interface { type Session interface {
HandleGlobalRequest(ch <-chan *ssh.Request) HandleGlobalRequest(ch <-chan *ssh.Request)
HandleTCPIPForward(req *ssh.Request) HandleTCPIPForward(req *ssh.Request)
@@ -28,80 +21,89 @@ type Session interface {
} }
type SSHSession struct { type SSHSession struct {
Lifecycle lifecycle.SessionLifecycle initialReq <-chan *ssh.Request
Interaction interaction.Controller sshReqChannel <-chan ssh.NewChannel
Forwarder forwarder.ForwardingController lifecycle lifecycle.SessionLifecycle
SlugManager slug.Manager interaction interaction.Controller
forwarder forwarder.ForwardingController
slugManager slug.Manager
registry Registry
} }
func New(conn *ssh.ServerConn, forwardingReq <-chan *ssh.Request, sshChan <-chan ssh.NewChannel) { func (s *SSHSession) GetLifecycle() lifecycle.SessionLifecycle {
return s.lifecycle
}
func (s *SSHSession) GetInteraction() interaction.Controller {
return s.interaction
}
func (s *SSHSession) GetForwarder() forwarder.ForwardingController {
return s.forwarder
}
func (s *SSHSession) GetSlugManager() slug.Manager {
return s.slugManager
}
func New(conn *ssh.ServerConn, forwardingReq <-chan *ssh.Request, sshChan <-chan ssh.NewChannel, sessionRegistry Registry) *SSHSession {
slugManager := slug.NewManager() slugManager := slug.NewManager()
forwarderManager := &forwarder.Forwarder{ forwarderManager := forwarder.NewForwarder(slugManager)
Listener: nil, interactionManager := interaction.NewInteraction(slugManager, forwarderManager)
TunnelType: "", lifecycleManager := lifecycle.NewLifecycle(conn, forwarderManager, slugManager)
ForwardedPort: 0,
SlugManager: slugManager,
}
interactionManager := &interaction.Interaction{
CommandBuffer: bytes.NewBuffer(make([]byte, 0, 20)),
InteractiveMode: false,
EditSlug: "",
SlugManager: slugManager,
Forwarder: forwarderManager,
Lifecycle: nil,
}
lifecycleManager := &lifecycle.Lifecycle{
Status: "",
Conn: conn,
Channel: nil,
Interaction: interactionManager,
Forwarder: forwarderManager,
SlugManager: slugManager,
}
interactionManager.SetLifecycle(lifecycleManager) interactionManager.SetLifecycle(lifecycleManager)
interactionManager.SetSlugModificator(updateClientSlug) interactionManager.SetSlugModificator(sessionRegistry.Update)
forwarderManager.SetLifecycle(lifecycleManager) forwarderManager.SetLifecycle(lifecycleManager)
lifecycleManager.SetUnregisterClient(unregisterClient) lifecycleManager.SetUnregisterClient(sessionRegistry.Remove)
session := &SSHSession{ return &SSHSession{
Lifecycle: lifecycleManager, initialReq: forwardingReq,
Interaction: interactionManager, sshReqChannel: sshChan,
Forwarder: forwarderManager, lifecycle: lifecycleManager,
SlugManager: slugManager, interaction: interactionManager,
} forwarder: forwarderManager,
slugManager: slugManager,
var once sync.Once registry: sessionRegistry,
for channel := range sshChan {
ch, reqs, err := channel.Accept()
if err != nil {
log.Printf("failed to accept channel: %v", err)
continue
}
once.Do(func() {
session.Lifecycle.SetChannel(ch)
session.Interaction.SetChannel(ch)
tcpipReq := session.waitForTCPIPForward(forwardingReq)
if tcpipReq == nil {
session.Interaction.SendMessage(fmt.Sprintf("Port forwarding request not received.\r\nEnsure you ran the correct command with -R flag.\r\nExample: ssh %s -p %s -R 80:localhost:3000\r\nFor more details, visit https://tunnl.live.\r\n\r\n", utils.Getenv("DOMAIN", "localhost"), utils.Getenv("PORT", "2200")))
if err := session.Lifecycle.Close(); err != nil {
log.Printf("failed to close session: %v", err)
}
return
}
session.HandleTCPIPForward(tcpipReq)
})
go session.HandleGlobalRequest(reqs)
}
if err := session.Lifecycle.Close(); err != nil {
log.Printf("failed to close session: %v", err)
} }
} }
func (s *SSHSession) waitForTCPIPForward(forwardingReq <-chan *ssh.Request) *ssh.Request { func (s *SSHSession) Start() error {
channel := <-s.sshReqChannel
ch, reqs, err := channel.Accept()
if err != nil {
log.Printf("failed to accept channel: %v", err)
return err
}
go s.HandleGlobalRequest(reqs)
tcpipReq := s.waitForTCPIPForward()
if tcpipReq == nil {
_, err := ch.Write([]byte(fmt.Sprintf("Port forwarding request not received. Ensure you ran the correct command with -R flag. Example: ssh %s -p %s -R 80:localhost:3000", config.Getenv("DOMAIN", "localhost"), config.Getenv("PORT", "2200"))))
if err != nil {
return err
}
if err := s.lifecycle.Close(); err != nil {
log.Printf("failed to close session: %v", err)
}
return fmt.Errorf("No forwarding Request")
}
s.lifecycle.SetChannel(ch)
s.interaction.SetChannel(ch)
s.HandleTCPIPForward(tcpipReq)
if err := s.lifecycle.Close(); err != nil {
log.Printf("failed to close session: %v", err)
return err
}
return nil
}
func (s *SSHSession) waitForTCPIPForward() *ssh.Request {
select { select {
case req, ok := <-forwardingReq: case req, ok := <-s.initialReq:
if !ok { if !ok {
log.Println("Forwarding request channel closed") log.Println("Forwarding request channel closed")
return nil return nil
@@ -119,41 +121,3 @@ func (s *SSHSession) waitForTCPIPForward(forwardingReq <-chan *ssh.Request) *ssh
return nil return nil
} }
} }
func updateClientSlug(oldSlug, newSlug string) bool {
clientsMutex.Lock()
defer clientsMutex.Unlock()
if _, exists := Clients[newSlug]; exists && newSlug != oldSlug {
return false
}
client, ok := Clients[oldSlug]
if !ok {
return false
}
delete(Clients, oldSlug)
client.SlugManager.Set(newSlug)
Clients[newSlug] = client
return true
}
func registerClient(slug string, session *SSHSession) bool {
clientsMutex.Lock()
defer clientsMutex.Unlock()
if _, exists := Clients[slug]; exists {
return false
}
Clients[slug] = session
return true
}
func unregisterClient(slug string) {
clientsMutex.Lock()
defer clientsMutex.Unlock()
delete(Clients, slug)
}

View File

@@ -1,32 +1,24 @@
package slug package slug
import "sync"
type Manager interface { type Manager interface {
Get() string Get() string
Set(slug string) Set(slug string)
} }
type manager struct { type manager struct {
slug string slug string
slugMu sync.RWMutex
} }
func NewManager() Manager { func NewManager() Manager {
return &manager{ return &manager{
slug: "", slug: "",
slugMu: sync.RWMutex{},
} }
} }
func (s *manager) Get() string { func (s *manager) Get() string {
s.slugMu.RLock()
defer s.slugMu.RUnlock()
return s.slug return s.slug
} }
func (s *manager) Set(slug string) { func (s *manager) Set(slug string) {
s.slugMu.Lock()
s.slug = slug s.slug = slug
s.slugMu.Unlock()
} }

View File

@@ -15,12 +15,6 @@ const (
TCP TunnelType = "TCP" TCP TunnelType = "TCP"
) )
type InteractionType string
const (
Slug InteractionType = "SLUG"
)
var BadGatewayResponse = []byte("HTTP/1.1 502 Bad Gateway\r\n" + var BadGatewayResponse = []byte("HTTP/1.1 502 Bad Gateway\r\n" +
"Content-Length: 11\r\n" + "Content-Length: 11\r\n" +
"Content-Type: text/plain\r\n\r\n" + "Content-Type: text/plain\r\n\r\n" +

17
version/version.go Normal file
View File

@@ -0,0 +1,17 @@
package version
import "fmt"
var (
Version = "dev"
BuildDate = "unknown"
Commit = "unknown"
)
func GetVersion() string {
return fmt.Sprintf("tunnel_pls %s (commit: %s, built: %s)", Version, Commit, BuildDate)
}
func GetShortVersion() string {
return Version
}