Compare commits
78 Commits
d2c5b2a4db
...
v1.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 7716eb7f29 | |||
| b115369913 | |||
| 9276430fae | |||
| f8a6f0bafe | |||
| acd02aadd3 | |||
| 878664e0ac | |||
| 20a88df330 | |||
| 075dd7ecad | |||
| ab34b34765 | |||
| 514c4f9de1 | |||
| d8330c684f | |||
| fbf182025b | |||
| 1038c0861e | |||
| 64e0d5805e | |||
| 85f21e7698 | |||
| 08565d845f | |||
| a7d9b2ab8a | |||
| bc8c5127a6 | |||
| a49b53e56f | |||
| e5b5cc3ae5 | |||
| b0b00764cf | |||
| 8b6cdef2e9 | |||
| 653517f5be | |||
| f11a92fb3b | |||
| ac283626d3 | |||
| ad7c5985b1 | |||
| 2644b4521c | |||
| d23ed27a4a | |||
| b5862bd7a0 | |||
| bf7f7bd8da | |||
| c3a469be64 | |||
| eee04daf80 | |||
| 1d918ef2aa | |||
| a2676a4f30 | |||
| 83657d3206 | |||
| 6710aec4bf | |||
| 0ca6285ef5 | |||
| 28cc069fdb | |||
| fe58e35e91 | |||
| 6cac64412c | |||
| 318003ac9f | |||
| 14fa237027 | |||
| 9a2a373eb3 | |||
| 1b248a2957 | |||
| 7348bdafb7 | |||
| cb8529f13e | |||
| fa6b097d66 | |||
| e3c4f59a77 | |||
| c69cd68820 | |||
| 76d1202b8e | |||
| 6dff735216 | |||
| 7bc5a01ba7 | |||
| 2a43f1441c | |||
| 6451304ed7 | |||
| 5c6826fe89 | |||
| 2725975d82 | |||
| b484981017 | |||
| 102c975388 | |||
| ad034ef681 | |||
| aceecfd14c | |||
| a2a688fc4e | |||
| 1de7155771 | |||
| c951c41a9b | |||
| 79d77497a0 | |||
| cb08bb7673 | |||
| 20b90c1727 | |||
| 5d9f7aee92 | |||
| a1e920f6b5 | |||
| dd96c8fe75 | |||
| b5045409cb | |||
| ba5f702e36 | |||
| 33e6ad08d7 | |||
| 8c8fdf251d | |||
| 03d3c8e4fb | |||
| fd513d7bc9 | |||
| 9bd2bead9e | |||
| 73e7df6a3b | |||
| 69c3e78728 |
32
.dockerignore
Normal file
32
.dockerignore
Normal 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
|
||||||
|
|
||||||
@@ -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,14 +35,29 @@ 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:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnl_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
|
||||||
@@ -40,6 +66,87 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnl_please:staging
|
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:staging
|
||||||
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/staging'
|
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'
|
||||||
|
|||||||
21
.gitea/workflows/renovate.yml
Normal file
21
.gitea/workflows/renovate.yml
Normal 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 }}
|
||||||
27
.github/workflows/sync-to-gitea.yml
vendored
Normal file
27
.github/workflows/sync-to-gitea.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Sync to Gitea
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.repository == 'fossyy/tunnel-please' &&
|
||||||
|
!contains(github.event.head_commit.message, '[skip-gitea]')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Push to Gitea
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
GITEA_URL: git.fossy.my.id
|
||||||
|
run: |
|
||||||
|
git remote add gitea https://${{ secrets.GITEA_USERNAME }}:${GITEA_TOKEN}@${GITEA_URL}/bagas/tunnel-please.git
|
||||||
|
git push gitea ${{ github.ref }} --force
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@ id_rsa*
|
|||||||
.idea
|
.idea
|
||||||
.env
|
.env
|
||||||
tmp
|
tmp
|
||||||
|
certs
|
||||||
|
app
|
||||||
55
Dockerfile
55
Dockerfile
@@ -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"]
|
||||||
|
|||||||
261
README.md
261
README.md
@@ -1,93 +1,240 @@
|
|||||||
# tunnel_pls
|
# Tunnel Please
|
||||||
|
|
||||||
|
A lightweight SSH-based tunnel server written in Go that enables secure TCP and HTTP forwarding with an interactive terminal interface for managing connections and custom subdomains.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
## Getting started
|
- SSH interactive session with real-time command handling
|
||||||
|
- Custom subdomain management for HTTP tunnels
|
||||||
|
- Dual protocol support: HTTP and TCP tunnels
|
||||||
|
- Real-time connection monitoring
|
||||||
|
## Requirements
|
||||||
|
|
||||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
- Go 1.18 or higher
|
||||||
|
- Valid domain name for subdomain routing
|
||||||
|
|
||||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
## Environment Variables
|
||||||
|
|
||||||
## Add your files
|
The following environment variables can be configured in the `.env` file:
|
||||||
|
|
||||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
| Variable | Description | Default | Required |
|
||||||
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
|----------|-------------|---------|----------|
|
||||||
|
| `DOMAIN` | Domain name for subdomain routing | `localhost` | 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_REDIRECT` | Redirect HTTP to HTTPS | `false` | 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) |
|
||||||
|
| `ACME_STAGING` | Use Let's Encrypt staging server | `false` | 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 |
|
||||||
|
| `BUFFER_SIZE` | Buffer size for io.Copy operations in bytes (4096-1048576) | `32768` | No |
|
||||||
|
| `PPROF_ENABLED` | Enable pprof profiling server | `false` | No |
|
||||||
|
| `PPROF_PORT` | Port for pprof server | `6060` | No |
|
||||||
|
|
||||||
```
|
**Note:** All environment variables now use UPPERCASE naming. The application includes sensible defaults for all variables, so you can run it without a `.env` file for basic functionality.
|
||||||
cd existing_repo
|
|
||||||
git remote add origin http://git.fossy.my.id/bagas/tunnel_pls.git
|
### Automatic TLS Certificate Management
|
||||||
git branch -M main
|
|
||||||
git push -uf origin main
|
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:**
|
||||||
|
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
|
||||||
|
3. Certificates are automatically renewed before expiration
|
||||||
|
4. User-provided certificates support hot-reload (changes detected every 30 seconds)
|
||||||
|
|
||||||
|
**Cloudflare API Token Setup:**
|
||||||
|
|
||||||
|
To use automatic certificate generation, you need a Cloudflare API token with the following permissions:
|
||||||
|
|
||||||
|
1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/profile/api-tokens)
|
||||||
|
2. Click "Create Token"
|
||||||
|
3. Use "Create Custom Token" with these permissions:
|
||||||
|
- **Zone → Zone → Read** (for all zones or specific zone)
|
||||||
|
- **Zone → DNS → Edit** (for all zones or specific zone)
|
||||||
|
4. Copy the token and set it as `CF_API_TOKEN` environment variable
|
||||||
|
|
||||||
|
**Example configuration for automatic certificates:**
|
||||||
|
```env
|
||||||
|
DOMAIN=example.com
|
||||||
|
TLS_ENABLED=true
|
||||||
|
CF_API_TOKEN=your_cloudflare_api_token_here
|
||||||
|
ACME_EMAIL=admin@example.com
|
||||||
|
# ACME_STAGING=true # Uncomment for testing to avoid rate limits
|
||||||
```
|
```
|
||||||
|
|
||||||
## Integrate with your tools
|
### SSH Key Auto-Generation
|
||||||
|
|
||||||
- [ ] [Set up project integrations](http://git.fossy.my.id/bagas/tunnel_pls/-/settings/integrations)
|
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.
|
||||||
|
|
||||||
## Collaborate with your team
|
### Memory Optimization
|
||||||
|
|
||||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
The application uses a buffer pool with controlled buffer sizes to prevent excessive memory usage under high concurrent loads. The `BUFFER_SIZE` environment variable controls the size of buffers used for io.Copy operations:
|
||||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
|
||||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
|
||||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
|
||||||
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
|
||||||
|
|
||||||
## Test and Deploy
|
- **Default:** 32768 bytes (32 KB) - Good balance for most scenarios
|
||||||
|
- **Minimum:** 4096 bytes (4 KB) - Lower memory usage, more CPU overhead
|
||||||
|
- **Maximum:** 1048576 bytes (1 MB) - Higher throughput, more memory usage
|
||||||
|
|
||||||
Use the built-in continuous integration in GitLab.
|
**Recommended settings based on load:**
|
||||||
|
- **Low traffic (<100 concurrent):** `BUFFER_SIZE=32768` (default)
|
||||||
|
- **High traffic (>100 concurrent):** `BUFFER_SIZE=16384` or `BUFFER_SIZE=8192`
|
||||||
|
- **Very high traffic (>1000 concurrent):** `BUFFER_SIZE=8192` or `BUFFER_SIZE=4096`
|
||||||
|
|
||||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
The buffer pool reuses buffers across connections, preventing memory fragmentation and reducing garbage collection pressure.
|
||||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
|
||||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
|
||||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
|
||||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
|
||||||
|
|
||||||
***
|
### Profiling with pprof
|
||||||
|
|
||||||
# Editing this README
|
To enable profiling for performance analysis:
|
||||||
|
|
||||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
1. Set `PPROF_ENABLED=true` in your `.env` file
|
||||||
|
2. Optionally set `PPROF_PORT` to your desired port (default: 6060)
|
||||||
|
3. Access profiling data at `http://localhost:6060/debug/pprof/`
|
||||||
|
|
||||||
## Suggestions for a good README
|
Common pprof endpoints:
|
||||||
|
- `/debug/pprof/` - Index page with available profiles
|
||||||
|
- `/debug/pprof/heap` - Memory allocation profile
|
||||||
|
- `/debug/pprof/goroutine` - Stack traces of all current goroutines
|
||||||
|
- `/debug/pprof/profile` - CPU profile (30-second sample by default)
|
||||||
|
- `/debug/pprof/trace` - Execution trace
|
||||||
|
|
||||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
Example usage with `go tool pprof`:
|
||||||
|
```bash
|
||||||
|
# Analyze CPU profile
|
||||||
|
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
|
||||||
|
|
||||||
## Name
|
# Analyze memory heap
|
||||||
Choose a self-explaining name for your project.
|
go tool pprof http://localhost:6060/debug/pprof/heap
|
||||||
|
```
|
||||||
|
|
||||||
## Description
|
## Docker Deployment
|
||||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
|
||||||
|
|
||||||
## Badges
|
Three Docker Compose configurations are available for different deployment scenarios. Each configuration uses the image `git.fossy.my.id/bagas/tunnel-please:latest`.
|
||||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
|
||||||
|
|
||||||
## Visuals
|
### Configuration Options
|
||||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
|
||||||
|
|
||||||
## Installation
|
#### 1. Root with Host Networking (RECOMMENDED)
|
||||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
|
||||||
|
|
||||||
## Usage
|
**File:** `docker-compose.root.yml`
|
||||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
|
||||||
|
|
||||||
## Support
|
**Advantages:**
|
||||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
- 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
|
||||||
|
|
||||||
## Roadmap
|
**Use Case:** Production deployments where you need unrestricted TCP forwarding and maximum performance.
|
||||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
|
||||||
|
**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
|
||||||
State if you are open to contributions and what your requirements are for accepting them.
|
Contributions are welcome!
|
||||||
|
|
||||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
If you'd like to contribute to this project, please follow the workflow below:
|
||||||
|
|
||||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
1. **Fork** the repository
|
||||||
|
2. Create a new branch for your changes
|
||||||
## Authors and acknowledgment
|
3. Commit and push your updates
|
||||||
Show your appreciation to those who have contributed to the project.
|
4. Open a **Pull Request** targeting the **`staging`** branch
|
||||||
|
5. Clearly describe your changes and the reasoning behind them
|
||||||
|
|
||||||
## License
|
## License
|
||||||
For open source projects, say how it is licensed.
|
This project is licensed under the [Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)](https://creativecommons.org/licenses/by-nc-nd/4.0/) license.
|
||||||
|
## Author
|
||||||
|
**Bagas (fossyy)**
|
||||||
|
|
||||||
|
- Website: [fossy.my.id](https://fossy.my.id)
|
||||||
|
- GitHub: [@fossyy](https://github.com/fossyy)
|
||||||
|
|
||||||
## Project status
|
|
||||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEWzCCA0OgAwIBAgIUDYTdEDHwVznxV/qnn0/WVlHNMZAwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwgZYxCzAJBgNVBAYTAklEMQ8wDQYDVQQIDAZCYW50ZW4xGjAYBgNVBAcMEVRh
|
|
||||||
bmdlcmFuZyBTZWxhdGFuMRIwEAYDVQQKDAlGb3NzeSBMVFMxDjAMBgNVBAsMBUZv
|
|
||||||
c3N5MRQwEgYDVQQDDAtmb3NzeS5teS5pZDEgMB4GCSqGSIb3DQEJARYRYmFnYXNA
|
|
||||||
Zm9zc3kubXkuaWQwHhcNMjUwMjA3MTYyMTU1WhcNMjYwMjA3MTYyMTU1WjCBljEL
|
|
||||||
MAkGA1UEBhMCSUQxDzANBgNVBAgMBkJhbnRlbjEaMBgGA1UEBwwRVGFuZ2VyYW5n
|
|
||||||
IFNlbGF0YW4xEjAQBgNVBAoMCUZvc3N5IExUUzEOMAwGA1UECwwFRm9zc3kxFDAS
|
|
||||||
BgNVBAMMC2Zvc3N5Lm15LmlkMSAwHgYJKoZIhvcNAQkBFhFiYWdhc0Bmb3NzeS5t
|
|
||||||
eS5pZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMU3pCA0eP+VR2CA
|
|
||||||
+p3p0BgKw33xCKQmQx52jnqNbvwW4NMMDc3mS6a+wb6QB6v0WLGMI1g22TtJPatx
|
|
||||||
7dcy4PCSOCV9xJ2Yfq5HlQgDHYoyE+juy4/pGlMjo45thJ0yI8zOSzaz2esosP22
|
|
||||||
XkfFwj7oVMXXPIY6UovcAlGU+DFtwVrNa76/esUwJs+7M3tBubjkpcal0wXR+SIX
|
|
||||||
3fmw5v0YzKV8qLGMGOvX6+OyLQCx4r9gB0d+WOrT6EfrPfAzo07NKjzWG9aWl1rk
|
|
||||||
Q+h0i38hke2MTFxId7za57L+NlXRjLo3ESbBF0hjYquTQOG3jx/UvWs+I1NEfsdu
|
|
||||||
vj8beiMCAwEAAaOBnjCBmzAfBgNVHSMEGDAWgBT6nj69+I+GSdgScjfOqnVFzX7G
|
|
||||||
aTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAs
|
|
||||||
BgNVHREEJTAjgglsb2NhbGhvc3SCCTEyNy4wLjAuMYILKi5sb2NhbGhvc3QwHQYD
|
|
||||||
VR0OBBYEFD9ci20pAUTRLVWxvVXjfE2GKwSAMA0GCSqGSIb3DQEBCwUAA4IBAQAT
|
|
||||||
rkjU+GzQy9B3/nd/79N7ozK80ygzmnRnj3ou/bbqUHOIYQQgKM1cuN6zh57ovRh/
|
|
||||||
u45s6pZUOUVN59POFUCvqUiOgsDkY/auXGbKtzzqzoZABuvm85ySd6zurOOx8tA5
|
|
||||||
e7ArX1ppy3LgzSb+cXANvzYC9bCwp70w2YylMFhwHBAp5TXRVqsqG6jujD8GMLoS
|
|
||||||
zDSDx8M6o8gqWmPQve7Saim9mgLJUvvCYBzvowuNjzZrJfAeGoIfLV127xCQiylm
|
|
||||||
fzUQ1Ac6udldWm9scA32nteSQMWg2d1nW3RG1nRondp13WUkgGQ490/c97D3MKLt
|
|
||||||
D1HB5dLIYkRVHVhCKHT1
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDjDCCAnSgAwIBAgIUPpuvw4ZdpnDBbwOZBt2ALfZykuYwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwPDELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9u
|
|
||||||
ZG9uMQswCQYDVQQLDAJIUTAeFw0yNDExMTkxNjQxNTNaFw0zNDExMTcxNjQxNTNa
|
|
||||||
MDwxCzAJBgNVBAYTAlVLMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRv
|
|
||||||
bjELMAkGA1UECwwCSFEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDD
|
|
||||||
L/aeaBzgkYxDNiQq7+nt6tKDfGnPDfBXunJlr1xbZfIVpJeDqwrNLWaZ0gtHci5E
|
|
||||||
sHptIpu5/uTBFaFyZVH604etY/YHIsff7BT2y32OobJYiKy2lvAI3/IDR4TGeDgA
|
|
||||||
HKryOwMcB5DheBVdeggxj36m8OjxhVADaiKp7BNXE2eqUqk8f2QpwqLQYe9UaU9r
|
|
||||||
WSJllNHOFk+RH17YBDiyyQ8CD1Vf5HcVSmPItWOMQytHcSgy0DHVXQCde86mky8t
|
|
||||||
8Ik74GeNrM6f+vWR4OfQ8dU2WSyTUE4c7czagkToheMX5fbbzWJkJcd2SD9wvyIk
|
|
||||||
tOot0YiZGQAoOedtGSEnAgMBAAGjgYUwgYIwCQYDVR0TBAIwADALBgNVHQ8EBAMC
|
|
||||||
BeAwSQYDVR0RBEIwQIIQbG9jYWxob3N0LmRpcmVjdIISKi5sb2NhbGhvc3QuZGly
|
|
||||||
ZWN0ghhTUy5jZXJ0LmxvY2FsaG9zdC5kaXJlY3QwHQYDVR0OBBYEFKBVeirQGZ4D
|
|
||||||
4AKVPd7LGfCF1wEZMA0GCSqGSIb3DQEBCwUAA4IBAQCRpvsc5DrQBo8yATmUS0OK
|
|
||||||
xfUXfZR28u3xYY+qMHi+ngIVT2TKJ1yoBJezV6WwQLkcGdWacULvPYt3jCFUtaP7
|
|
||||||
+hzfs5y1FssFsXDx+r3pQxYyE6BX3BhlrJPJhLRyG1siTTgN439Qu40/TsTzNgAT
|
|
||||||
A9GbAro+W6+qA4H92mBlyfQEOBossID/Kk95uvDnQguUOp0ZBLgFNRfE6Ra9+yC+
|
|
||||||
ufAOksYDrisPE6kZId0Ra4Ln/GmrIXKjjmLCitq2q2REC/70JSCnaJcBYeThSJLZ
|
|
||||||
AZ24AF+JteakSJ8FEgRGxvSu0wdZfnMobNoelsjai1p5l5mCTiD8GH9sQCkslnp3
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDL/aeaBzgkYxD
|
|
||||||
NiQq7+nt6tKDfGnPDfBXunJlr1xbZfIVpJeDqwrNLWaZ0gtHci5EsHptIpu5/uTB
|
|
||||||
FaFyZVH604etY/YHIsff7BT2y32OobJYiKy2lvAI3/IDR4TGeDgAHKryOwMcB5Dh
|
|
||||||
eBVdeggxj36m8OjxhVADaiKp7BNXE2eqUqk8f2QpwqLQYe9UaU9rWSJllNHOFk+R
|
|
||||||
H17YBDiyyQ8CD1Vf5HcVSmPItWOMQytHcSgy0DHVXQCde86mky8t8Ik74GeNrM6f
|
|
||||||
+vWR4OfQ8dU2WSyTUE4c7czagkToheMX5fbbzWJkJcd2SD9wvyIktOot0YiZGQAo
|
|
||||||
OedtGSEnAgMBAAECggEASXibb2MnQ4zl7ELL+HGYb5sNpLrHJU5M4ujmuMn6jNjh
|
|
||||||
+C2dbs2KYlMtpMcAweMD8Y0weDYnwiplNx0KSYJECpNnJehTqrn33J0EAyXz3CWX
|
|
||||||
eWXxBUXpkp2hfoSEQSTth3VDD60Q7ZMXgRdvi2EtBmLKPNLADHGu/aoM5ENdwE9/
|
|
||||||
E80X68E+MT2czOY/sEI/w2Tf/S2ZVOHRvFOsmvTLFlbiElWG8pmguajpJwdae7uX
|
|
||||||
c5VD87b0oYicSUvaHe6xOCCyyeBVq06sWk+vh6Tzrw6K/B+q0SYvzXdrdsJxbzUD
|
|
||||||
PvhVi9rf9AC1Ncb6lFOP2ZjGfqYQvfgGXKqaVxXWQQKBgQD8frATynMSeR15oERa
|
|
||||||
+Maa7r3GlwWV3tkUblX7/FxBo9UhAsivnWZprccZR43YPowbWNeU3AQppf/YDxmx
|
|
||||||
70/RVTCMjXPGyspSS2iFtcp+K2KnIZA4BuAG/s1EBrAUW2KrtaTaXgYu9usgb+Fq
|
|
||||||
dJUEBDWrL59XXtQUwSM1laH0BwKBgQDF5Z31VY96xOS7flmolq8Ag+frSMH0G4np
|
|
||||||
3nUnTlUkgpFXE5FkmUccbYZv9QialAVriBBUNANPDBQH4PRrNnX8Z6B2HJbpAy30
|
|
||||||
II9jPMTNKnXT3RKFcJCamNkOQhT0sBAwm4gTsx+7gbpdZxinxH0Cr/08sfU4lbee
|
|
||||||
EtMV/J1h4QKBgC4MLLBvS20jCW0U/WJZ3F6FC7cb87jRW2WOeb/q1ihiaIwMpezh
|
|
||||||
F7xOJPFHS2cUgRi7qxVKyreNvor4tgbtTfEvSBtZ8LNgaGV5uyYncTZxUxyH0nVl
|
|
||||||
S5X7AhRV4+bSg7ws9FOesiH+hgL0ZHe1qzeATQlbNgQJF0RxtKohD9ghAoGAcM0N
|
|
||||||
WIZInoYUivreSEZ7wiNt0qNKSsZXukLfLGRuC72Q8r1opprn+cBEXRSirtmorT6F
|
|
||||||
cDmlmS0dTdBgAaytXA4FXM23B2KUkw7sLHi7BOcq+nSM1hrvke+F6aapI0AoOkyt
|
|
||||||
J+12LP8pJ4xYdWh+iUWfZzVYvcQ5QZUhVOsFGoECgYA9llKmc/cFaXrCr97g7ls8
|
|
||||||
ZWW1kQKLawAv6+90dwECJl+zpyQN/TURyEfz6oFJDbNEL0xAAcm9thDah177Tq95
|
|
||||||
pcHbVn/pAI4h4CLzM2ExSe8Ybvy6iPZaBiCVXq3ms1PK0EDyYLH1p3FZqm+UStZh
|
|
||||||
/6fYspyivrQEivRK3jGuWA==
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDFN6QgNHj/lUdg
|
|
||||||
gPqd6dAYCsN98QikJkMedo56jW78FuDTDA3N5kumvsG+kAer9FixjCNYNtk7ST2r
|
|
||||||
ce3XMuDwkjglfcSdmH6uR5UIAx2KMhPo7suP6RpTI6OObYSdMiPMzks2s9nrKLD9
|
|
||||||
tl5HxcI+6FTF1zyGOlKL3AJRlPgxbcFazWu+v3rFMCbPuzN7Qbm45KXGpdMF0fki
|
|
||||||
F935sOb9GMylfKixjBjr1+vjsi0AseK/YAdHfljq0+hH6z3wM6NOzSo81hvWlpda
|
|
||||||
5EPodIt/IZHtjExcSHe82uey/jZV0Yy6NxEmwRdIY2Krk0Dht48f1L1rPiNTRH7H
|
|
||||||
br4/G3ojAgMBAAECggEAATiJ23ZhS1++squ87jsgAfR+TYP8Kpv408u/43Ig5KgC
|
|
||||||
ZnwPUWqvP2e0TLwyhMKwXxHMt2nITxQlSnyCXU/5nk07BYxkqhiwEni4Xo86YK+H
|
|
||||||
rQXEnKFaSHdF6dNNIo+VZiark4adS9XgJp0fsn0LnON7GhBt72MvPW6auxH1HJIC
|
|
||||||
rcjnzJu/KzTVrId9QNsEDl9cNRlHXuPSfohdq2o39PBKGDeDEDTvP9wHtasEF6oe
|
|
||||||
uj1OH5fAxiXptT0Ln0Y8QeFe8R8Odul5mUXCgMOkKmHZPDxTq9ldCuPjrz15JfnS
|
|
||||||
T3MecaWdvChpIP2WybjLstJy9qXTl0fhRkpJWpaVbQKBgQDu9puq64vZn0jak7ns
|
|
||||||
bFbKPemFw7tiiwwnqyzqCTRddw1vETJN/MgpRWvc7d1Cvzs2+Z2kn0Cgpdw/30Ej
|
|
||||||
VRHiE/d1rj+u33F6vGBUBccRue1t0UdFMFF/YnlNDRRnljQgx4N3mezzNE2RikR8
|
|
||||||
jVp+jvWTSTgf3y4Hip1ffCSNtQKBgQDTRx1huSzUOed/36YLdnxP+AlV3L+c/EKU
|
|
||||||
GItKZk2bqRlqgs6wcsNlVjveb9o9j6M5pg/lh5HaqJyhj8DYVnEdmXfcQJiUfIPh
|
|
||||||
5802asIXQVBmL9rTMR34wsr/lzzK3k0KORqFcUdf2fDI/UQNpVUJvWQQY8fl2sVo
|
|
||||||
zmp88JAPdwKBgQCLC6fsvn5ztMF5nffTX/7oUzosgYXpgyshcfMCgzSbJgkFFaaF
|
|
||||||
xo7ZpPFsbmQO0KMuC/T0s02xrJEKAWgvnPJ48FFPgoK/yHiJiE8s1OfOordK7Tlh
|
|
||||||
QwpI6w3WDcRPuhC++hi/YSuFIGv6QdA0ATQk7B5tA2/K69wmuztzMhM6+QKBgH5E
|
|
||||||
LwQbRfZj0L20bKjHHA4y32loLz/j5upZLM2/DDyuN9lW6a28OJiUi90pHdXSxSsL
|
|
||||||
2s5DUmDKiiloH0lrh9i3wlFobYe4Tp0xCoyuCucZCrK3gODcptvnlqhfu15GsuYc
|
|
||||||
MIR1qcFYH7YO3qAFIiha/rVo3Ku7LmWvjyayInaLAoGAZ/dELEAcyhImf1HvtmBT
|
|
||||||
qNg4uI6t/2fvHdoAeQkkGjEDWFTGEaMp0cJEbETPD68TwNh5dL/xUJibTwMbK+m5
|
|
||||||
rdIA2oTZMkFtWubIvMCrD/J6E+8Pzl+eK+0C0axO/I29S4veORNGWvCtqdFICDgZ
|
|
||||||
CluJ0NZFeMH8g8tgfI9HnHc=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
37
docker-compose.root.yml
Normal file
37
docker-compose.root.yml
Normal 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
|
||||||
|
|
||||||
39
docker-compose.standard.yml
Normal file
39
docker-compose.standard.yml
Normal 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
40
docker-compose.tcp.yml
Normal 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
|
||||||
|
|
||||||
42
go.mod
42
go.mod
@@ -3,8 +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 golang.org/x/sys v0.38.0 // indirect
|
require (
|
||||||
|
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/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/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/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
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
|
golang.org/x/mod v0.30.0 // indirect
|
||||||
|
golang.org/x/net v0.47.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
|
golang.org/x/text v0.32.0 // indirect
|
||||||
|
golang.org/x/tools v0.39.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
116
go.sum
116
go.sum
@@ -1,13 +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/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
|
||||||
|
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/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=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
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/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||||
|
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||||
|
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/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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
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/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||||
|
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||||
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
|
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/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
|
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
35
internal/config/config.go
Normal 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
|
||||||
|
}
|
||||||
67
internal/key/key.go
Normal file
67
internal/key/key.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package key
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateSSHKeyIfNotExist(keyPath string) error {
|
||||||
|
if _, err := os.Stat(keyPath); err == nil {
|
||||||
|
log.Printf("SSH key already exists at %s", keyPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("SSH key not found at %s, generating new key pair...", keyPath)
|
||||||
|
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyPEM := &pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(keyPath)
|
||||||
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyFile, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer privateKeyFile.Close()
|
||||||
|
|
||||||
|
if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKeyPath := keyPath + ".pub"
|
||||||
|
pubKeyFile, err := os.OpenFile(pubKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer pubKeyFile.Close()
|
||||||
|
|
||||||
|
_, err = pubKeyFile.Write(ssh.MarshalAuthorizedKey(publicKey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("SSH key pair generated successfully at %s and %s", keyPath, pubKeyPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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")
|
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
18
internal/random/random.go
Normal 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()
|
||||||
|
}
|
||||||
54
main.go
54
main.go
@@ -1,37 +1,69 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
_ "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() {
|
||||||
sshConfig := &ssh.ServerConfig{
|
if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-v") {
|
||||||
NoClientAuth: true,
|
fmt.Println(version.GetVersion())
|
||||||
ServerVersion: "SSH-2.0-TunnlPls-1.0",
|
os.Exit(0)
|
||||||
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
|
||||||
return nil, nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
|
||||||
privateBytes, err := os.ReadFile(utils.Getenv("ssh_private_key"))
|
log.Printf("Starting %s", version.GetVersion())
|
||||||
|
|
||||||
|
pprofEnabled := config.Getenv("PPROF_ENABLED", "false")
|
||||||
|
if pprofEnabled == "true" {
|
||||||
|
pprofPort := config.Getenv("PPROF_PORT", "6060")
|
||||||
|
go func() {
|
||||||
|
pprofAddr := fmt.Sprintf("localhost:%s", pprofPort)
|
||||||
|
log.Printf("Starting pprof server on http://%s/debug/pprof/", pprofAddr)
|
||||||
|
if err := http.ListenAndServe(pprofAddr, nil); err != nil {
|
||||||
|
log.Printf("pprof server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
sshConfig := &ssh.ServerConfig{
|
||||||
|
NoClientAuth: true,
|
||||||
|
ServerVersion: fmt.Sprintf("SSH-2.0-TunnlPls-%s", version.GetShortVersion()),
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKeyPath := "certs/ssh/id_rsa"
|
||||||
|
if err := key.GenerateSSHKeyIfNotExist(sshKeyPath); err != nil {
|
||||||
|
log.Fatalf("Failed to generate SSH key: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
privateBytes, err := os.ReadFile(sshKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load private key : %s", err.Error())
|
log.Fatalf("Failed to load private key: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
private, err := ssh.ParsePrivateKey(privateBytes)
|
private, err := ssh.ParsePrivateKey(privateBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to parse private key")
|
log.Fatalf("Failed to parse private key: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
8
renovate-config.js
Normal 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
19
renovate.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"minor",
|
||||||
|
"patch",
|
||||||
|
"pin",
|
||||||
|
"digest"
|
||||||
|
],
|
||||||
|
"automerge": true,
|
||||||
|
"baseBranchPatterns": [
|
||||||
|
"staging"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
197
server/header.go
197
server/header.go
@@ -4,8 +4,6 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type HeaderManager interface {
|
type HeaderManager interface {
|
||||||
@@ -15,64 +13,169 @@ 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(r io.Reader) (*RequestHeaderFactory, error) {
|
func NewRequestHeaderFactory(r interface{}) (RequestHeaderManager, error) {
|
||||||
br := bufio.NewReader(r)
|
switch v := r.(type) {
|
||||||
header := &RequestHeaderFactory{
|
case []byte:
|
||||||
headers: make(map[string]string),
|
return parseHeadersFromBytes(v)
|
||||||
|
case *bufio.Reader:
|
||||||
|
return parseHeadersFromReader(v)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported type: %T", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHeadersFromBytes(headerData []byte) (RequestHeaderManager, error) {
|
||||||
|
header := &requestHeaderFactory{
|
||||||
|
headers: make(map[string]string, 16),
|
||||||
}
|
}
|
||||||
|
|
||||||
startLine, err := br.ReadString('\n')
|
lineEnd := bytes.IndexByte(headerData, '\n')
|
||||||
if err != nil {
|
if lineEnd == -1 {
|
||||||
return nil, err
|
return nil, fmt.Errorf("invalid request: no newline found")
|
||||||
}
|
}
|
||||||
startLine = strings.TrimRight(startLine, "\r\n")
|
|
||||||
header.startLine = []byte(startLine)
|
|
||||||
|
|
||||||
parts := strings.Split(startLine, " ")
|
startLine := bytes.TrimRight(headerData[:lineEnd], "\r\n")
|
||||||
|
header.startLine = make([]byte, len(startLine))
|
||||||
|
copy(header.startLine, startLine)
|
||||||
|
|
||||||
|
parts := bytes.Split(startLine, []byte{' '})
|
||||||
if len(parts) < 3 {
|
if len(parts) < 3 {
|
||||||
return nil, fmt.Errorf("invalid request line")
|
return nil, fmt.Errorf("invalid request line")
|
||||||
}
|
}
|
||||||
|
|
||||||
header.Method = parts[0]
|
header.method = string(parts[0])
|
||||||
header.Path = parts[1]
|
header.path = string(parts[1])
|
||||||
header.Version = parts[2]
|
header.version = string(parts[2])
|
||||||
|
|
||||||
for {
|
remaining := headerData[lineEnd+1:]
|
||||||
line, err := br.ReadString('\n')
|
|
||||||
if err != nil {
|
for len(remaining) > 0 {
|
||||||
return nil, err
|
lineEnd = bytes.IndexByte(remaining, '\n')
|
||||||
|
if lineEnd == -1 {
|
||||||
|
lineEnd = len(remaining)
|
||||||
}
|
}
|
||||||
line = strings.TrimRight(line, "\r\n")
|
|
||||||
|
|
||||||
if line == "" {
|
line := bytes.TrimRight(remaining[:lineEnd], "\r\n")
|
||||||
|
|
||||||
|
if len(line) == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
kv := strings.SplitN(line, ":", 2)
|
colonIdx := bytes.IndexByte(line, ':')
|
||||||
if len(kv) != 2 {
|
if colonIdx != -1 {
|
||||||
continue
|
key := bytes.TrimSpace(line[:colonIdx])
|
||||||
|
value := bytes.TrimSpace(line[colonIdx+1:])
|
||||||
|
header.headers[string(key)] = string(value)
|
||||||
}
|
}
|
||||||
header.headers[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
|
|
||||||
|
if lineEnd == len(remaining) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
remaining = remaining[lineEnd+1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
return header, nil
|
return header, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResponseHeaderFactory(startLine []byte) *ResponseHeaderFactory {
|
func parseHeadersFromReader(br *bufio.Reader) (RequestHeaderManager, error) {
|
||||||
header := &ResponseHeaderFactory{
|
header := &requestHeaderFactory{
|
||||||
|
headers: make(map[string]string, 16),
|
||||||
|
}
|
||||||
|
|
||||||
|
startLineBytes, err := br.ReadSlice('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == bufio.ErrBufferFull {
|
||||||
|
var startLine string
|
||||||
|
startLine, err = br.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
startLineBytes = []byte(startLine)
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startLineBytes = bytes.TrimRight(startLineBytes, "\r\n")
|
||||||
|
header.startLine = make([]byte, len(startLineBytes))
|
||||||
|
copy(header.startLine, startLineBytes)
|
||||||
|
|
||||||
|
parts := bytes.Split(startLineBytes, []byte{' '})
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return nil, fmt.Errorf("invalid request line")
|
||||||
|
}
|
||||||
|
|
||||||
|
header.method = string(parts[0])
|
||||||
|
header.path = string(parts[1])
|
||||||
|
header.version = string(parts[2])
|
||||||
|
|
||||||
|
for {
|
||||||
|
lineBytes, err := br.ReadSlice('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == bufio.ErrBufferFull {
|
||||||
|
var line string
|
||||||
|
line, err = br.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lineBytes = []byte(line)
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lineBytes = bytes.TrimRight(lineBytes, "\r\n")
|
||||||
|
|
||||||
|
if len(lineBytes) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
colonIdx := bytes.IndexByte(lineBytes, ':')
|
||||||
|
if colonIdx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := bytes.TrimSpace(lineBytes[:colonIdx])
|
||||||
|
value := bytes.TrimSpace(lineBytes[colonIdx+1:])
|
||||||
|
|
||||||
|
header.headers[string(key)] = string(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return header, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponseHeaderFactory(startLine []byte) ResponseHeaderManager {
|
||||||
|
header := &responseHeaderFactory{
|
||||||
startLine: nil,
|
startLine: nil,
|
||||||
headers: make(map[string]string),
|
headers: make(map[string]string),
|
||||||
}
|
}
|
||||||
@@ -98,19 +201,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)
|
||||||
@@ -127,7 +230,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 ""
|
||||||
@@ -135,22 +238,32 @@ 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)
|
||||||
buf.WriteString("\r\n")
|
buf.WriteString("\r\n")
|
||||||
|
|
||||||
req.headers["X-HF"] = "modified"
|
|
||||||
|
|
||||||
for key, val := range req.headers {
|
for key, val := range req.headers {
|
||||||
buf.WriteString(key)
|
buf.WriteString(key)
|
||||||
buf.WriteString(": ")
|
buf.WriteString(": ")
|
||||||
|
|||||||
309
server/http.go
309
server/http.go
@@ -10,36 +10,65 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
"tunnel_pls/internal/config"
|
||||||
"tunnel_pls/session"
|
"tunnel_pls/session"
|
||||||
"tunnel_pls/types"
|
|
||||||
"tunnel_pls/utils"
|
"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 err != nil {
|
if read == 0 && err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +77,9 @@ func (cw *CustomWriter) Read(p []byte) (int, error) {
|
|||||||
idx := bytes.Index(tmp, DELIMITER)
|
idx := bytes.Index(tmp, DELIMITER)
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
copy(p, tmp)
|
copy(p, tmp)
|
||||||
|
if err != nil {
|
||||||
|
return read, err
|
||||||
|
}
|
||||||
return read, nil
|
return read, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,55 +92,51 @@ func (cw *CustomWriter) Read(p []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range cw.reqEndMW {
|
for _, m := range cw.reqEndMW {
|
||||||
err := m.HandleRequest(cw.reqHeader)
|
err = m.HandleRequest(cw.reqHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error when applying request middleware: %v", err)
|
log.Printf("Error when applying request middleware: %v", err)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headerReader := bufio.NewReader(bytes.NewReader(header))
|
reqhf, err := NewRequestHeaderFactory(header)
|
||||||
reqhf, err := NewRequestHeaderFactory(headerReader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range cw.reqStartMW {
|
for _, m := range cw.reqStartMW {
|
||||||
err := m.HandleRequest(reqhf)
|
if mwErr := m.HandleRequest(reqhf); mwErr != nil {
|
||||||
if err != nil {
|
log.Printf("Error when applying request middleware: %v", mwErr)
|
||||||
log.Printf("Error when applying request middleware: %v", err)
|
return 0, mwErr
|
||||||
return 0, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cw.reqHeader = reqhf
|
cw.reqHeader = reqhf
|
||||||
finalHeader := reqhf.Finalize()
|
finalHeader := reqhf.Finalize()
|
||||||
|
|
||||||
n := copy(p, finalHeader)
|
combined := append(finalHeader, body...)
|
||||||
n += copy(p[n:], body)
|
|
||||||
|
n := copy(p, combined)
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var DELIMITER = []byte{0x0D, 0x0A, 0x0D, 0x0A} // HTTP HEADER DELIMITER `\r\n\r\n`
|
var DELIMITER = []byte{0x0D, 0x0A, 0x0D, 0x0A}
|
||||||
var requestLine = regexp.MustCompile(`^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|TRACE|CONNECT) \S+ HTTP/\d\.\d$`)
|
var requestLine = regexp.MustCompile(`^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|TRACE|CONNECT) \S+ HTTP/\d\.\d$`)
|
||||||
var responseLine = regexp.MustCompile(`^HTTP/\d\.\d \d{3} .+`)
|
var responseLine = regexp.MustCompile(`^HTTP/\d\.\d \d{3} .+`)
|
||||||
|
|
||||||
func isHTTPHeader(buf []byte) bool {
|
func isHTTPHeader(buf []byte) bool {
|
||||||
lines := bytes.Split(buf, []byte("\r\n"))
|
lines := bytes.Split(buf, []byte("\r\n"))
|
||||||
if len(lines) < 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
startLine := string(lines[0])
|
startLine := string(lines[0])
|
||||||
if !requestLine.MatchString(startLine) && !responseLine.MatchString(startLine) {
|
if !requestLine.MatchString(startLine) && !responseLine.MatchString(startLine) {
|
||||||
return false
|
return false
|
||||||
@@ -118,83 +146,100 @@ func isHTTPHeader(buf []byte) bool {
|
|||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !bytes.Contains(line, []byte(":")) {
|
colonIdx := bytes.IndexByte(line, ':')
|
||||||
|
if colonIdx <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cw *CustomWriter) Write(p []byte) (int, error) {
|
func (cw *customWriter) Write(p []byte) (int, error) {
|
||||||
if len(p) == len(types.BadGatewayResponse) && bytes.Equal(p, types.BadGatewayResponse) {
|
if cw.respHeader != nil && len(cw.buf) == 0 && len(p) >= 5 && string(p[0:5]) == "HTTP/" {
|
||||||
return cw.writer.Write(p)
|
cw.respHeader = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cw.respHeader != nil {
|
||||||
|
n, err := cw.writer.Write(p)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cw.buf = append(cw.buf, p...)
|
cw.buf = append(cw.buf, p...)
|
||||||
// TODO: implement middleware buat cache system dll
|
|
||||||
if idx := bytes.Index(cw.buf, DELIMITER); idx != -1 {
|
|
||||||
header := cw.buf[:idx+len(DELIMITER)]
|
|
||||||
body := cw.buf[idx+len(DELIMITER):]
|
|
||||||
|
|
||||||
if isHTTPHeader(header) {
|
idx := bytes.Index(cw.buf, DELIMITER)
|
||||||
resphf := NewResponseHeaderFactory(header)
|
if idx == -1 {
|
||||||
for _, m := range cw.respMW {
|
return len(p), nil
|
||||||
err := m.HandleResponse(resphf, body)
|
}
|
||||||
if err != nil {
|
|
||||||
log.Printf("Cannot apply middleware: %s\n", err)
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
header = resphf.Finalize()
|
|
||||||
cw.respHeader = resphf
|
|
||||||
_, err := cw.writer.Write(header)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(body) > 0 {
|
header := cw.buf[:idx+len(DELIMITER)]
|
||||||
_, err := cw.writer.Write(body)
|
body := cw.buf[idx+len(DELIMITER):]
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
if !isHTTPHeader(header) {
|
||||||
}
|
_, err := cw.writer.Write(cw.buf)
|
||||||
}
|
cw.buf = nil
|
||||||
cw.buf = nil
|
if err != nil {
|
||||||
return len(p), nil
|
return 0, err
|
||||||
}
|
}
|
||||||
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cw.buf = nil
|
resphf := NewResponseHeaderFactory(header)
|
||||||
n, err := cw.writer.Write(p)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
for _, m := range cw.respMW {
|
for _, m := range cw.respMW {
|
||||||
err := m.HandleResponse(cw.respHeader, p)
|
err := m.HandleResponse(resphf, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Cannot apply middleware: %s\n", err)
|
log.Printf("Cannot apply middleware: %s\n", err)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return n, nil
|
header = resphf.Finalize()
|
||||||
}
|
cw.respHeader = resphf
|
||||||
|
|
||||||
func (cw *CustomWriter) AddInteraction(interaction Interaction) {
|
_, err := cw.writer.Write(header)
|
||||||
cw.interaction = interaction
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(body) > 0 {
|
||||||
|
_, err = cw.writer.Write(body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cw.buf = nil
|
||||||
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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") == "true" && utils.Getenv("tls_redirect") == "true" {
|
if config.Getenv("TLS_ENABLED", "false") == "true" && config.Getenv("TLS_REDIRECT", "false") == "true" {
|
||||||
redirectTLS = true
|
redirectTLS = true
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
conn, err := listener.Accept()
|
var conn net.Conn
|
||||||
|
conn, err = listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, net.ErrClosed) {
|
if errors.Is(err, net.ErrClosed) {
|
||||||
return
|
return
|
||||||
@@ -203,13 +248,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) {
|
||||||
@@ -239,8 +284,8 @@ func Handler(conn net.Conn) {
|
|||||||
slug := host[0]
|
slug := host[0]
|
||||||
|
|
||||||
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")) +
|
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"))
|
||||||
@@ -252,8 +297,7 @@ func Handler(conn net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if slug == "ping" {
|
if slug == "ping" {
|
||||||
// TODO: implement cors
|
_, err = conn.Write([]byte(
|
||||||
_, err := conn.Write([]byte(
|
|
||||||
"HTTP/1.1 200 OK\r\n" +
|
"HTTP/1.1 200 OK\r\n" +
|
||||||
"Content-Length: 0\r\n" +
|
"Content-Length: 0\r\n" +
|
||||||
"Connection: close\r\n" +
|
"Connection: close\r\n" +
|
||||||
@@ -269,9 +313,9 @@ 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" +
|
||||||
"Connection: close\r\n" +
|
"Connection: close\r\n" +
|
||||||
@@ -280,59 +324,68 @@ func Handler(conn net.Conn) {
|
|||||||
log.Println("Failed to write 301 Moved Permanently:", err)
|
log.Println("Failed to write 301 Moved Permanently:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = conn.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to close connection:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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())
|
||||||
channel, reqs, err := sshSession.Lifecycle.GetConnection().OpenChannel("forwarded-tcpip", payload)
|
|
||||||
if err != nil {
|
type channelResult struct {
|
||||||
log.Printf("Failed to open forwarded-tcpip channel: %v", err)
|
channel ssh.Channel
|
||||||
return
|
reqs <-chan *ssh.Request
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
resultChan := make(chan channelResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for req := range reqs {
|
channel, reqs, err := sshSession.GetLifecycle().GetConnection().OpenChannel("forwarded-tcpip", payload)
|
||||||
err := req.Reply(false, nil)
|
resultChan <- channelResult{channel, reqs, err}
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to reply to request: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
_, err = channel.Write(initialRequest.Finalize())
|
|
||||||
if err != nil {
|
var channel ssh.Channel
|
||||||
log.Printf("Failed to forward request: %v", err)
|
var reqs <-chan *ssh.Request
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-resultChan:
|
||||||
|
if result.err != nil {
|
||||||
|
log.Printf("Failed to open forwarded-tcpip channel: %v", result.err)
|
||||||
|
sshSession.GetForwarder().WriteBadGatewayResponse(cw.GetWriter())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channel = result.channel
|
||||||
|
reqs = result.reqs
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
log.Printf("Timeout opening forwarded-tcpip channel")
|
||||||
|
sshSession.GetForwarder().WriteBadGatewayResponse(cw.GetWriter())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//TODO: Implement wrapper func buat add/remove middleware
|
|
||||||
|
go ssh.DiscardRequests(reqs)
|
||||||
|
|
||||||
fingerprintMiddleware := NewTunnelFingerprint()
|
fingerprintMiddleware := NewTunnelFingerprint()
|
||||||
loggerMiddleware := NewRequestLogger(cw.interaction, cw.RemoteAddr)
|
forwardedForMiddleware := NewForwardedFor(cw.GetRemoteAddr())
|
||||||
cw.respMW = append(cw.respMW, fingerprintMiddleware)
|
|
||||||
cw.reqStartMW = append(cw.reqStartMW, loggerMiddleware)
|
|
||||||
|
|
||||||
//TODO: Tambah req Middleware
|
cw.AddResponseMiddleware(fingerprintMiddleware)
|
||||||
cw.reqEndMW = nil
|
cw.AddRequestStartMiddleware(forwardedForMiddleware)
|
||||||
cw.reqHeader = initialRequest
|
cw.SetRequestHeader(initialRequest)
|
||||||
|
|
||||||
for _, m := range cw.reqStartMW {
|
for _, m := range cw.GetRequestStartMiddleware() {
|
||||||
err := m.HandleRequest(cw.reqHeader)
|
if err := m.HandleRequest(initialRequest); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error handling request: %v", err)
|
log.Printf("Error handling request: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sshSession.Forwarder.HandleConnection(cw, channel, cw.RemoteAddr)
|
_, err := channel.Write(initialRequest.Finalize())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to forward request: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sshSession.GetForwarder().HandleConnection(cw, channel, cw.GetRemoteAddr())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,25 +8,27 @@ 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 {
|
||||||
cert, err := tls.LoadX509KeyPair(utils.Getenv("cert_loc"), utils.Getenv("key_loc"))
|
domain := config.Getenv("DOMAIN", "localhost")
|
||||||
|
httpsPort := config.Getenv("HTTPS_PORT", "8443")
|
||||||
|
|
||||||
|
tlsConfig, err := NewTLSConfig(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to initialize TLS config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &tls.Config{Certificates: []tls.Certificate{cert}}
|
ln, err := tls.Listen("tcp", ":"+httpsPort, tlsConfig)
|
||||||
ln, err := tls.Listen("tcp", ":443", config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
conn, err := ln.Accept()
|
var conn net.Conn
|
||||||
|
conn, err = ln.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, net.ErrClosed) {
|
if errors.Is(err, net.ErrClosed) {
|
||||||
log.Println("https server closed")
|
log.Println("https server closed")
|
||||||
@@ -35,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 {
|
||||||
@@ -60,24 +62,18 @@ func HandlerTLS(conn net.Conn) {
|
|||||||
|
|
||||||
host := strings.Split(reqhf.Get("Host"), ".")
|
host := strings.Split(reqhf.Get("Host"), ".")
|
||||||
if len(host) < 1 {
|
if len(host) < 1 {
|
||||||
_, err := conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
|
_, err = conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to write 400 Bad Request:", err)
|
log.Println("Failed to write 400 Bad Request:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = conn.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to close connection:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
slug := host[0]
|
slug := host[0]
|
||||||
|
|
||||||
if slug == "ping" {
|
if slug == "ping" {
|
||||||
// TODO: implement cors
|
_, err = conn.Write([]byte(
|
||||||
_, err := conn.Write([]byte(
|
|
||||||
"HTTP/1.1 200 OK\r\n" +
|
"HTTP/1.1 200 OK\r\n" +
|
||||||
"Content-Length: 0\r\n" +
|
"Content-Length: 0\r\n" +
|
||||||
"Connection: close\r\n" +
|
"Connection: close\r\n" +
|
||||||
@@ -93,9 +89,9 @@ 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" +
|
||||||
"Connection: close\r\n" +
|
"Connection: close\r\n" +
|
||||||
@@ -104,15 +100,9 @@ func HandlerTLS(conn net.Conn) {
|
|||||||
log.Println("Failed to write 301 Moved Permanently:", err)
|
log.Println("Failed to write 301 Moved Permanently:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = conn.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to close connection:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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{}
|
||||||
@@ -19,145 +17,25 @@ type TunnelFingerprint struct{}
|
|||||||
func NewTunnelFingerprint() *TunnelFingerprint {
|
func NewTunnelFingerprint() *TunnelFingerprint {
|
||||||
return &TunnelFingerprint{}
|
return &TunnelFingerprint{}
|
||||||
}
|
}
|
||||||
func (h *TunnelFingerprint) HandleRequest(header *RequestHeaderFactory) error {
|
|
||||||
return nil
|
func (h *TunnelFingerprint) HandleResponse(header ResponseHeaderManager, body []byte) error {
|
||||||
}
|
|
||||||
func (h *TunnelFingerprint) HandleResponse(header *ResponseHeaderFactory, body []byte) error {
|
|
||||||
header.Set("Server", "Tunnel Please")
|
header.Set("Server", "Tunnel Please")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestLogger struct {
|
type ForwardedFor struct {
|
||||||
interaction Interaction
|
addr net.Addr
|
||||||
remoteAddr net.Addr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRequestLogger(interaction Interaction, remoteAddr net.Addr) *RequestLogger {
|
func NewForwardedFor(addr net.Addr) *ForwardedFor {
|
||||||
return &RequestLogger{
|
return &ForwardedFor{addr: addr}
|
||||||
interaction: interaction,
|
}
|
||||||
remoteAddr: remoteAddr,
|
|
||||||
|
func (ff *ForwardedFor) HandleRequest(header RequestHeaderManager) error {
|
||||||
|
host, _, err := net.SplitHostPort(ff.addr.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
header.Set("X-Forwarded-For", host)
|
||||||
|
|
||||||
func (rl *RequestLogger) HandleRequest(header *RequestHeaderFactory) error {
|
|
||||||
rl.interaction.SendMessage(fmt.Sprintf("\033[32m%s %s -> %s %s \033[0m\r\n", time.Now().UTC().Format(time.RFC3339), rl.remoteAddr.String(), header.Method, header.Path))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *RequestLogger) HandleResponse(header *ResponseHeaderFactory, body []byte) error { return nil }
|
|
||||||
|
|
||||||
//TODO: Implement caching atau enggak
|
|
||||||
//const maxCacheSize = 50 * 1024 * 1024
|
|
||||||
//
|
|
||||||
//type DiskCacheMiddleware struct {
|
|
||||||
// dir string
|
|
||||||
// mu sync.Mutex
|
|
||||||
// file *os.File
|
|
||||||
// path string
|
|
||||||
// cacheable bool
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func NewDiskCacheMiddleware() *DiskCacheMiddleware {
|
|
||||||
// return &DiskCacheMiddleware{dir: "cache"}
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func (c *DiskCacheMiddleware) ensureDir() error {
|
|
||||||
// return os.MkdirAll(c.dir, 0755)
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func (c *DiskCacheMiddleware) cacheKey(method, path string) string {
|
|
||||||
// return fmt.Sprintf("%s_%s.cache", method, base64.URLEncoding.EncodeToString([]byte(path)))
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func (c *DiskCacheMiddleware) filePath(method, path string) string {
|
|
||||||
// return filepath.Join(c.dir, c.cacheKey(method, path))
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func fileExists(path string) bool {
|
|
||||||
// _, err := os.Stat(path)
|
|
||||||
// if err == nil {
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
// if os.IsNotExist(err) {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// return false
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func canCacheRequest(header *RequestHeaderFactory) bool {
|
|
||||||
// if header.Method != "GET" {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if cacheControl := header.Get("Cache-Control"); cacheControl != "" {
|
|
||||||
// if strings.Contains(cacheControl, "no-store") || strings.Contains(cacheControl, "private") || strings.Contains(cacheControl, "no-cache") || strings.Contains(cacheControl, "max-age=0") {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if header.Get("Authorization") != "" {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if header.Get("Cookie") != "" {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return true
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func (c *DiskCacheMiddleware) HandleRequest(header *RequestHeaderFactory) error {
|
|
||||||
// if !canCacheRequest(header) {
|
|
||||||
// c.cacheable = false
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// c.cacheable = true
|
|
||||||
// _ = c.ensureDir()
|
|
||||||
// path := c.filePath(header.Method, header.Path)
|
|
||||||
//
|
|
||||||
// if fileExists(path + ".finish") {
|
|
||||||
// c.file = nil
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if c.file != nil {
|
|
||||||
// err := c.file.Close()
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// err = os.Rename(c.path, c.path+".finish")
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// c.path = path
|
|
||||||
// f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// c.file = f
|
|
||||||
//
|
|
||||||
// return nil
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func (c *DiskCacheMiddleware) HandleResponse(header *ResponseHeaderFactory, body []byte) error {
|
|
||||||
// if !c.cacheable {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if c.file == nil {
|
|
||||||
// header.Set("X-Cache", "HIT")
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// _, err := c.file.Write(body)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// header.Set("X-Cache", "MISS")
|
|
||||||
// return nil
|
|
||||||
//}
|
|
||||||
|
|||||||
@@ -4,49 +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")))
|
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") == "true" {
|
|
||||||
go func() {
|
HttpServer := NewHTTPServer(sessionRegistry)
|
||||||
err := NewHTTPSServer()
|
err = HttpServer.ListenAndServe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to start https server: %v", err)
|
log.Fatalf("failed to start http server: %v", err)
|
||||||
}
|
return nil, err
|
||||||
return
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
go func() {
|
|
||||||
err := NewHTTPServer()
|
if config.Getenv("TLS_ENABLED", "false") == "true" {
|
||||||
|
err = HttpServer.ListenAndServeTLS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to start http server: %v", err)
|
log.Fatalf("failed to start https server: %v", err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
return &Server{
|
|
||||||
Conn: &listener,
|
|
||||||
Config: &config,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &Server{
|
||||||
|
conn: &listener,
|
||||||
|
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
|
||||||
@@ -55,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
|
||||||
|
}
|
||||||
|
|||||||
321
server/tls.go
Normal file
321
server/tls.go
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"tunnel_pls/internal/config"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/cloudflare"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
certPath string
|
||||||
|
keyPath string
|
||||||
|
storagePath string
|
||||||
|
|
||||||
|
userCert *tls.Certificate
|
||||||
|
userCertMu sync.RWMutex
|
||||||
|
|
||||||
|
magic *certmagic.Config
|
||||||
|
|
||||||
|
useCertMagic bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalTLSManager TLSManager
|
||||||
|
var tlsManagerOnce sync.Once
|
||||||
|
|
||||||
|
func NewTLSConfig(domain string) (*tls.Config, error) {
|
||||||
|
var initErr error
|
||||||
|
|
||||||
|
tlsManagerOnce.Do(func() {
|
||||||
|
certPath := "certs/tls/cert.pem"
|
||||||
|
keyPath := "certs/tls/privkey.pem"
|
||||||
|
storagePath := "certs/tls/certmagic"
|
||||||
|
|
||||||
|
tm := &tlsManager{
|
||||||
|
domain: domain,
|
||||||
|
certPath: certPath,
|
||||||
|
keyPath: keyPath,
|
||||||
|
storagePath: storagePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
if tm.userCertsExistAndValid() {
|
||||||
|
log.Printf("Using user-provided certificates from %s and %s", certPath, keyPath)
|
||||||
|
if err := tm.loadUserCerts(); err != nil {
|
||||||
|
initErr = fmt.Errorf("failed to load user certificates: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tm.useCertMagic = false
|
||||||
|
tm.startCertWatcher()
|
||||||
|
} else {
|
||||||
|
if !isACMEConfigComplete() {
|
||||||
|
log.Printf("User certificates missing or invalid, and ACME configuration is incomplete")
|
||||||
|
log.Printf("To enable automatic certificate generation, set CF_API_TOKEN environment variable")
|
||||||
|
initErr = fmt.Errorf("no valid certificates found and ACME configuration is incomplete (CF_API_TOKEN is required)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("User certificates missing or don't cover %s and *.%s, using CertMagic", domain, domain)
|
||||||
|
if err := tm.initCertMagic(); err != nil {
|
||||||
|
initErr = fmt.Errorf("failed to initialize CertMagic: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tm.useCertMagic = true
|
||||||
|
}
|
||||||
|
|
||||||
|
globalTLSManager = tm
|
||||||
|
})
|
||||||
|
|
||||||
|
if initErr != nil {
|
||||||
|
return nil, initErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalTLSManager.getTLSConfig(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isACMEConfigComplete() bool {
|
||||||
|
cfAPIToken := config.Getenv("CF_API_TOKEN", "")
|
||||||
|
return cfAPIToken != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *tlsManager) userCertsExistAndValid() bool {
|
||||||
|
if _, err := os.Stat(tm.certPath); os.IsNotExist(err) {
|
||||||
|
log.Printf("Certificate file not found: %s", tm.certPath)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(tm.keyPath); os.IsNotExist(err) {
|
||||||
|
log.Printf("Key file not found: %s", tm.keyPath)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateCertDomains(tm.certPath, tm.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateCertDomains(certPath, domain string) bool {
|
||||||
|
certPEM, err := os.ReadFile(certPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to read certificate: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(certPEM)
|
||||||
|
if block == nil {
|
||||||
|
log.Printf("Failed to decode PEM block from certificate")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to parse certificate: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(cert.NotAfter) {
|
||||||
|
log.Printf("Certificate has expired (NotAfter: %v)", cert.NotAfter)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Add(30 * 24 * time.Hour).After(cert.NotAfter) {
|
||||||
|
log.Printf("Certificate expiring soon (NotAfter: %v), will use CertMagic for renewal", cert.NotAfter)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var certDomains []string
|
||||||
|
if cert.Subject.CommonName != "" {
|
||||||
|
certDomains = append(certDomains, cert.Subject.CommonName)
|
||||||
|
}
|
||||||
|
certDomains = append(certDomains, cert.DNSNames...)
|
||||||
|
|
||||||
|
hasBase := false
|
||||||
|
hasWildcard := false
|
||||||
|
wildcardDomain := "*." + domain
|
||||||
|
|
||||||
|
for _, d := range certDomains {
|
||||||
|
if d == domain {
|
||||||
|
hasBase = true
|
||||||
|
}
|
||||||
|
if d == wildcardDomain {
|
||||||
|
hasWildcard = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasBase {
|
||||||
|
log.Printf("Certificate does not cover base domain: %s", domain)
|
||||||
|
}
|
||||||
|
if !hasWildcard {
|
||||||
|
log.Printf("Certificate does not cover wildcard domain: %s", wildcardDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasBase && hasWildcard
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *tlsManager) loadUserCerts() error {
|
||||||
|
cert, err := tls.LoadX509KeyPair(tm.certPath, tm.keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.userCertMu.Lock()
|
||||||
|
tm.userCert = &cert
|
||||||
|
tm.userCertMu.Unlock()
|
||||||
|
|
||||||
|
log.Printf("Loaded user certificates successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *tlsManager) startCertWatcher() {
|
||||||
|
go func() {
|
||||||
|
var lastCertMod, lastKeyMod time.Time
|
||||||
|
|
||||||
|
if info, err := os.Stat(tm.certPath); err == nil {
|
||||||
|
lastCertMod = info.ModTime()
|
||||||
|
}
|
||||||
|
if info, err := os.Stat(tm.keyPath); err == nil {
|
||||||
|
lastKeyMod = info.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
certInfo, certErr := os.Stat(tm.certPath)
|
||||||
|
keyInfo, keyErr := os.Stat(tm.keyPath)
|
||||||
|
|
||||||
|
if certErr != nil || keyErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if certInfo.ModTime().After(lastCertMod) || keyInfo.ModTime().After(lastKeyMod) {
|
||||||
|
log.Printf("Certificate files changed, reloading...")
|
||||||
|
|
||||||
|
if !ValidateCertDomains(tm.certPath, tm.domain) {
|
||||||
|
log.Printf("New certificates don't cover required domains")
|
||||||
|
|
||||||
|
if !isACMEConfigComplete() {
|
||||||
|
log.Printf("Cannot switch to CertMagic: ACME configuration is incomplete (CF_API_TOKEN is required)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Switching to CertMagic for automatic certificate management")
|
||||||
|
if err := tm.initCertMagic(); err != nil {
|
||||||
|
log.Printf("Failed to initialize CertMagic: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tm.useCertMagic = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tm.loadUserCerts(); err != nil {
|
||||||
|
log.Printf("Failed to reload certificates: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCertMod = certInfo.ModTime()
|
||||||
|
lastKeyMod = keyInfo.ModTime()
|
||||||
|
log.Printf("Certificates reloaded successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *tlsManager) initCertMagic() error {
|
||||||
|
if err := os.MkdirAll(tm.storagePath, 0700); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cert storage directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acmeEmail := config.Getenv("ACME_EMAIL", "admin@"+tm.domain)
|
||||||
|
cfAPIToken := config.Getenv("CF_API_TOKEN", "")
|
||||||
|
acmeStaging := config.Getenv("ACME_STAGING", "false") == "true"
|
||||||
|
|
||||||
|
if cfAPIToken == "" {
|
||||||
|
return fmt.Errorf("CF_API_TOKEN environment variable is required for automatic certificate generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfProvider := &cloudflare.Provider{
|
||||||
|
APIToken: cfAPIToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
storage := &certmagic.FileStorage{Path: tm.storagePath}
|
||||||
|
|
||||||
|
cache := certmagic.NewCache(certmagic.CacheOptions{
|
||||||
|
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
|
||||||
|
return tm.magic, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
magic := certmagic.New(cache, certmagic.Config{
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
|
||||||
|
acmeIssuer := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
|
||||||
|
Email: acmeEmail,
|
||||||
|
Agreed: true,
|
||||||
|
DNS01Solver: &certmagic.DNS01Solver{
|
||||||
|
DNSManager: certmagic.DNSManager{
|
||||||
|
DNSProvider: cfProvider,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if acmeStaging {
|
||||||
|
acmeIssuer.CA = certmagic.LetsEncryptStagingCA
|
||||||
|
log.Printf("Using Let's Encrypt staging server")
|
||||||
|
} else {
|
||||||
|
acmeIssuer.CA = certmagic.LetsEncryptProductionCA
|
||||||
|
log.Printf("Using Let's Encrypt production server")
|
||||||
|
}
|
||||||
|
|
||||||
|
magic.Issuers = []certmagic.Issuer{acmeIssuer}
|
||||||
|
tm.magic = magic
|
||||||
|
|
||||||
|
domains := []string{tm.domain, "*." + tm.domain}
|
||||||
|
log.Printf("Requesting certificates for: %v", domains)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := magic.ManageSync(ctx, domains); err != nil {
|
||||||
|
return fmt.Errorf("failed to obtain certificates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Certificates obtained successfully for %v", domains)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *tlsManager) getTLSConfig() *tls.Config {
|
||||||
|
return &tls.Config{
|
||||||
|
GetCertificate: tm.getCertificate,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *tlsManager) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
if tm.useCertMagic {
|
||||||
|
return tm.magic.GetCertificate(hello)
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.userCertMu.RLock()
|
||||||
|
defer tm.userCertMu.RUnlock()
|
||||||
|
|
||||||
|
if tm.userCert == nil {
|
||||||
|
return nil, fmt.Errorf("no certificate available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tm.userCert, nil
|
||||||
|
}
|
||||||
@@ -8,18 +8,44 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"tunnel_pls/internal/config"
|
||||||
"tunnel_pls/session/slug"
|
"tunnel_pls/session/slug"
|
||||||
"tunnel_pls/types"
|
"tunnel_pls/types"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var bufferPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
bufSize := config.GetBufferSize()
|
||||||
|
return make([]byte, bufSize)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyWithBuffer(dst io.Writer, src io.Reader) (written int64, err error) {
|
||||||
|
buf := bufferPool.Get().([]byte)
|
||||||
|
defer bufferPool.Put(buf)
|
||||||
|
return io.CopyBuffer(dst, src, buf)
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -42,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() {
|
||||||
@@ -55,28 +81,57 @@ func (f *Forwarder) AcceptTCPConnections() {
|
|||||||
log.Printf("Error accepting connection: %v", err)
|
log.Printf("Error accepting connection: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
payload := f.CreateForwardedTCPIPPayload(conn.RemoteAddr())
|
|
||||||
channel, reqs, err := f.Lifecycle.GetConnection().OpenChannel("forwarded-tcpip", payload)
|
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||||
if err != nil {
|
log.Printf("Failed to set connection deadline: %v", err)
|
||||||
log.Printf("Failed to open forwarded-tcpip channel: %v", err)
|
if closeErr := conn.Close(); closeErr != nil {
|
||||||
return
|
log.Printf("Failed to close connection: %v", closeErr)
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payload := f.CreateForwardedTCPIPPayload(conn.RemoteAddr())
|
||||||
|
|
||||||
|
type channelResult struct {
|
||||||
|
channel ssh.Channel
|
||||||
|
reqs <-chan *ssh.Request
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
resultChan := make(chan channelResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for req := range reqs {
|
channel, reqs, err := f.lifecycle.GetConnection().OpenChannel("forwarded-tcpip", payload)
|
||||||
err := req.Reply(false, nil)
|
resultChan <- channelResult{channel, reqs, err}
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to reply to request: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
go f.HandleConnection(conn, channel, conn.RemoteAddr())
|
|
||||||
|
select {
|
||||||
|
case result := <-resultChan:
|
||||||
|
if result.err != nil {
|
||||||
|
log.Printf("Failed to open forwarded-tcpip channel: %v", result.err)
|
||||||
|
if closeErr := conn.Close(); closeErr != nil {
|
||||||
|
log.Printf("Failed to close connection: %v", closeErr)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.SetDeadline(time.Time{}); err != nil {
|
||||||
|
log.Printf("Failed to clear connection deadline: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go ssh.DiscardRequests(result.reqs)
|
||||||
|
go f.HandleConnection(conn, result.channel, conn.RemoteAddr())
|
||||||
|
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
log.Printf("Timeout opening forwarded-tcpip channel")
|
||||||
|
if closeErr := conn.Close(); closeErr != nil {
|
||||||
|
log.Printf("Failed to close connection: %v", closeErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Forwarder) HandleConnection(dst io.ReadWriter, src ssh.Channel, remoteAddr net.Addr) {
|
func (f *Forwarder) HandleConnection(dst io.ReadWriter, src ssh.Channel, remoteAddr net.Addr) {
|
||||||
defer func(src ssh.Channel) {
|
defer func() {
|
||||||
_, err := io.Copy(io.Discard, src)
|
_, err := io.Copy(io.Discard, src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to discard connection: %v", err)
|
log.Printf("Failed to discard connection: %v", err)
|
||||||
@@ -84,48 +139,63 @@ func (f *Forwarder) HandleConnection(dst io.ReadWriter, src ssh.Channel, remoteA
|
|||||||
|
|
||||||
err = src.Close()
|
err = src.Close()
|
||||||
if err != nil && !errors.Is(err, io.EOF) {
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
log.Printf("Error closing connection: %v", err)
|
log.Printf("Error closing source channel: %v", err)
|
||||||
}
|
}
|
||||||
}(src)
|
|
||||||
log.Printf("Handling new forwarded connection from %s", remoteAddr)
|
|
||||||
|
|
||||||
go func() {
|
if closer, ok := dst.(io.Closer); ok {
|
||||||
_, err := io.Copy(src, dst)
|
err = closer.Close()
|
||||||
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
log.Printf("Error copying from conn.Reader to channel: %v", err)
|
log.Printf("Error closing destination connection: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
_, err := io.Copy(dst, src)
|
log.Printf("Handling new forwarded connection from %s", remoteAddr)
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, io.EOF) {
|
var wg sync.WaitGroup
|
||||||
log.Printf("Error copying from channel to conn.Writer: %v", err)
|
wg.Add(2)
|
||||||
}
|
|
||||||
return
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
_, err := copyWithBuffer(dst, src)
|
||||||
|
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
|
||||||
|
log.Printf("Error copying src→dst: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
_, err := copyWithBuffer(src, dst)
|
||||||
|
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
|
||||||
|
log.Printf("Error copying dst→src: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -137,8 +207,8 @@ func (f *Forwarder) WriteBadGatewayResponse(dst io.Writer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Forwarder) Close() error {
|
func (f *Forwarder) Close() error {
|
||||||
if f.GetTunnelType() != types.HTTP {
|
if f.GetListener() != nil {
|
||||||
return f.Listener.Close()
|
return f.listener.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,10 +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 "tcpip-forward":
|
case "shell", "pty-req":
|
||||||
s.HandleTCPIPForward(req)
|
err := req.Reply(true, nil)
|
||||||
return
|
if err != nil {
|
||||||
case "shell", "pty-req", "window-change":
|
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)
|
||||||
@@ -52,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)
|
||||||
}
|
}
|
||||||
@@ -62,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)
|
||||||
}
|
}
|
||||||
@@ -76,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)
|
||||||
}
|
}
|
||||||
@@ -90,163 +106,161 @@ 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)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Interaction.SendMessage("\033[H\033[2J")
|
|
||||||
s.Lifecycle.SetStatus(types.RUNNING)
|
|
||||||
go s.Interaction.HandleUserInput()
|
|
||||||
|
|
||||||
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 {
|
||||||
if err != nil {
|
log.Printf("Port %d is already in use or restricted", portToBind)
|
||||||
log.Println("Failed to set port status:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.HandleTCPForward(req, addr, portToBind)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
|
|
||||||
s.Forwarder.SetType(types.HTTP)
|
|
||||||
s.Forwarder.SetForwardedPort(portToBind)
|
|
||||||
|
|
||||||
slug := generateUniqueSlug()
|
|
||||||
if 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)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
err = s.lifecycle.Close()
|
||||||
}
|
|
||||||
|
|
||||||
s.SlugManager.Set(slug)
|
|
||||||
registerClient(slug, s)
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
err := binary.Write(buf, binary.BigEndian, uint32(80))
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to reply to request:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("HTTP forwarding approved on port: %d", 80)
|
|
||||||
|
|
||||||
domain := utils.Getenv("domain")
|
|
||||||
protocol := "http"
|
|
||||||
if utils.Getenv("tls_enabled") == "true" {
|
|
||||||
protocol = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Interaction.ShowWelcomeMessage()
|
|
||||||
s.Interaction.SendMessage(fmt.Sprintf("Forwarding your traffic to %s://%s.%s\r\n", protocol, slug, domain))
|
|
||||||
err = req.Reply(true, buf.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to reply to request:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind uint16) {
|
|
||||||
s.Forwarder.SetType(types.TCP)
|
|
||||||
log.Printf("Requested forwarding on %s:%d", addr, portToBind)
|
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", portToBind))
|
|
||||||
if err != nil {
|
|
||||||
s.Interaction.SendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port.\r\n", portToBind))
|
|
||||||
err := req.Reply(false, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to reply to request:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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
|
||||||
}
|
}
|
||||||
s.Forwarder.SetListener(listener)
|
err = portUtil.Default.SetPortStatus(portToBind, true)
|
||||||
s.Forwarder.SetForwardedPort(portToBind)
|
if err != nil {
|
||||||
s.Interaction.ShowWelcomeMessage()
|
log.Println("Failed to set port status:", err)
|
||||||
s.Interaction.SendMessage(fmt.Sprintf("Forwarding your traffic to tcp://%s:%d \r\n", utils.Getenv("domain"), s.Forwarder.GetForwardedPort()))
|
return
|
||||||
|
}
|
||||||
|
|
||||||
go s.Forwarder.AcceptTCPConnections()
|
s.HandleTCPForward(req, addr, portToBind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
|
||||||
|
slug := random.GenerateRandomString(20)
|
||||||
|
|
||||||
|
if !s.registry.Register(slug, s) {
|
||||||
|
log.Printf("Failed to register client with slug: %s", slug)
|
||||||
|
err := req.Reply(false, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to reply to request:", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err := binary.Write(buf, binary.BigEndian, uint32(portToBind))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write port to buffer:", err)
|
||||||
|
s.registry.Remove(slug)
|
||||||
|
err = req.Reply(false, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to reply to request:", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("HTTP forwarding approved on port: %d", portToBind)
|
||||||
|
|
||||||
|
err = req.Reply(true, buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to reply to request:", err)
|
||||||
|
s.registry.Remove(slug)
|
||||||
|
err = req.Reply(false, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to reply to request:", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.forwarder.SetType(types.HTTP)
|
||||||
|
s.forwarder.SetForwardedPort(portToBind)
|
||||||
|
s.slugManager.Set(slug)
|
||||||
|
s.lifecycle.SetStatus(types.RUNNING)
|
||||||
|
s.interaction.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind uint16) {
|
||||||
|
log.Printf("Requested forwarding on %s:%d", addr, portToBind)
|
||||||
|
listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", portToBind))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Port %d is already in use or restricted", portToBind)
|
||||||
|
if setErr := portUtil.Default.SetPortStatus(portToBind, false); setErr != nil {
|
||||||
|
log.Printf("Failed to reset port status: %v", setErr)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
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 reply to request:", err)
|
log.Println("Failed to write port to buffer:", err)
|
||||||
|
if setErr := portUtil.Default.SetPortStatus(portToBind, false); setErr != nil {
|
||||||
|
log.Printf("Failed to reset port status: %v", setErr)
|
||||||
|
}
|
||||||
|
err = listener.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to close listener: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("TCP forwarding approved on port: %d", portToBind)
|
log.Printf("TCP forwarding approved on port: %d", 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.Default.SetPortStatus(portToBind, false); setErr != nil {
|
||||||
|
log.Printf("Failed to reset port status: %v", setErr)
|
||||||
|
}
|
||||||
|
err = listener.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to close listener: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func generateUniqueSlug() string {
|
s.forwarder.SetType(types.TCP)
|
||||||
maxAttempts := 5
|
s.forwarder.SetListener(listener)
|
||||||
|
s.forwarder.SetForwardedPort(portToBind)
|
||||||
for i := 0; i < maxAttempts; i++ {
|
s.lifecycle.SetStatus(types.RUNNING)
|
||||||
slug := utils.GenerateRandomString(20)
|
go s.forwarder.AcceptTCPConnections()
|
||||||
|
s.interaction.Start()
|
||||||
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) {
|
||||||
|
|||||||
152
session/interaction/constants.go
Normal file
152
session/interaction/constants.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package interaction
|
||||||
|
|
||||||
|
const (
|
||||||
|
backspaceChar = 8
|
||||||
|
deleteChar = 127
|
||||||
|
enterChar = 13
|
||||||
|
escapeChar = 27
|
||||||
|
ctrlC = 3
|
||||||
|
forwardSlash = '/'
|
||||||
|
minPrintableChar = 32
|
||||||
|
maxPrintableChar = 126
|
||||||
|
|
||||||
|
minSlugLength = 3
|
||||||
|
maxSlugLength = 20
|
||||||
|
|
||||||
|
clearScreen = "\033[H\033[2J"
|
||||||
|
clearLine = "\033[K"
|
||||||
|
clearToLineEnd = "\r\033[K"
|
||||||
|
backspaceSeq = "\b \b"
|
||||||
|
|
||||||
|
minBoxWidth = 50
|
||||||
|
paddingRight = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var forbiddenSlugs = map[string]struct{}{
|
||||||
|
"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
@@ -2,11 +2,8 @@ package lifecycle
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
|
||||||
portUtil "tunnel_pls/internal/port"
|
portUtil "tunnel_pls/internal/port"
|
||||||
"tunnel_pls/session/slug"
|
"tunnel_pls/session/slug"
|
||||||
"tunnel_pls/types"
|
"tunnel_pls/types"
|
||||||
@@ -14,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
|
||||||
@@ -25,23 +18,31 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionLifecycle interface {
|
type SessionLifecycle interface {
|
||||||
Close() error
|
Close() error
|
||||||
WaitForRunningStatus()
|
|
||||||
SetStatus(status types.Status)
|
SetStatus(status types.Status)
|
||||||
GetConnection() ssh.Conn
|
GetConnection() ssh.Conn
|
||||||
GetChannel() ssh.Channel
|
GetChannel() ssh.Channel
|
||||||
@@ -50,73 +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) WaitForRunningStatus() {
|
|
||||||
timeout := time.After(3 * time.Second)
|
|
||||||
ticker := time.NewTicker(150 * time.Millisecond)
|
|
||||||
defer ticker.Stop()
|
|
||||||
frames := []string{"-", "\\", "|", "/"}
|
|
||||||
i := 0
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
l.Interaction.SendMessage(fmt.Sprintf("\rLoading %s", frames[i]))
|
|
||||||
i = (i + 1) % len(frames)
|
|
||||||
if l.Status == types.RUNNING {
|
|
||||||
l.Interaction.SendMessage("\r\033[K")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-timeout:
|
|
||||||
l.Interaction.SendMessage("\r\033[K")
|
|
||||||
l.Interaction.SendMessage("TCP/IP request not received in time.\r\nCheck your internet connection and confirm the server responds within 3000ms.\r\nEnsure you ran the correct command. For more details, visit https://tunnl.live.\r\n\r\n")
|
|
||||||
err := l.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to close session: %v", err)
|
|
||||||
}
|
|
||||||
log.Println("Timeout waiting for session to start running")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
66
session/registry.go
Normal 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 ®istry{
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"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/types"
|
|
||||||
|
|
||||||
"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)
|
||||||
@@ -26,104 +21,103 @@ 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)),
|
|
||||||
EditMode: 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,
|
||||||
|
registry: sessionRegistry,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
func (s *SSHSession) Start() error {
|
||||||
go session.Lifecycle.WaitForRunningStatus()
|
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)
|
||||||
|
|
||||||
for channel := range sshChan {
|
tcpipReq := s.waitForTCPIPForward()
|
||||||
ch, reqs, _ := channel.Accept()
|
if tcpipReq == nil {
|
||||||
if session.Lifecycle.GetChannel() == 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"))))
|
||||||
session.Lifecycle.SetChannel(ch)
|
|
||||||
session.Interaction.SetChannel(ch)
|
|
||||||
session.Lifecycle.SetStatus(types.SETUP)
|
|
||||||
go session.HandleGlobalRequest(forwardingReq)
|
|
||||||
}
|
|
||||||
go session.HandleGlobalRequest(reqs)
|
|
||||||
}
|
|
||||||
err := session.Lifecycle.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.lifecycle.Close(); err != nil {
|
||||||
log.Printf("failed to close session: %v", err)
|
log.Printf("failed to close session: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return fmt.Errorf("No forwarding Request")
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateClientSlug(oldSlug, newSlug string) bool {
|
|
||||||
clientsMutex.Lock()
|
|
||||||
defer clientsMutex.Unlock()
|
|
||||||
|
|
||||||
if _, exists := Clients[newSlug]; exists && newSlug != oldSlug {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client, ok := Clients[oldSlug]
|
s.lifecycle.SetChannel(ch)
|
||||||
if !ok {
|
s.interaction.SetChannel(ch)
|
||||||
return false
|
|
||||||
|
s.HandleTCPIPForward(tcpipReq)
|
||||||
|
|
||||||
|
if err := s.lifecycle.Close(); err != nil {
|
||||||
|
log.Printf("failed to close session: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
delete(Clients, oldSlug)
|
|
||||||
client.SlugManager.Set(newSlug)
|
|
||||||
Clients[newSlug] = client
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerClient(slug string, session *SSHSession) bool {
|
func (s *SSHSession) waitForTCPIPForward() *ssh.Request {
|
||||||
clientsMutex.Lock()
|
select {
|
||||||
defer clientsMutex.Unlock()
|
case req, ok := <-s.initialReq:
|
||||||
|
if !ok {
|
||||||
if _, exists := Clients[slug]; exists {
|
log.Println("Forwarding request channel closed")
|
||||||
return false
|
return nil
|
||||||
|
}
|
||||||
|
if req.Type == "tcpip-forward" {
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
if err := req.Reply(false, nil); err != nil {
|
||||||
|
log.Printf("Failed to reply to request: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Expected tcpip-forward request, got: %s", req.Type)
|
||||||
|
return nil
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
log.Println("No forwarding request received")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
Clients[slug] = session
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func unregisterClient(slug string) {
|
|
||||||
clientsMutex.Lock()
|
|
||||||
defer clientsMutex.Unlock()
|
|
||||||
|
|
||||||
delete(Clients, slug)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 := rand.New(rand.NewSource(time.Now().UnixNano() + int64(rand.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 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)
|
|
||||||
env.value[key] = val
|
|
||||||
|
|
||||||
if val == "" {
|
|
||||||
panic("Asking for env: " + key + " but got nothing, please set your environment first")
|
|
||||||
}
|
|
||||||
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
17
version/version.go
Normal file
17
version/version.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user