Compare commits
103 Commits
659f6c3ee7
...
v1.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| d666ae5545 | |||
| 5edb3c8086 | |||
| 5b603d8317 | |||
| 8fd9f8b567 | |||
| 30e84ac3b7 | |||
| fd6ffc2500 | |||
| e1cd4ed981 | |||
| 96d2b88f95 | |||
| 2e8767f17a | |||
| 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 | |||
| d2c5b2a4db | |||
| 0b8bc1dbba | |||
| 43178d51b5 | |||
| 368cc0b3e3 | |||
| 7819dcc430 | |||
| af951b8fa7 | |||
| 990bccbff7 | |||
| 36790cab44 | |||
| cad22cd25a | |||
| 0cb02f5220 | |||
| 7bee2f2c9b | |||
| 54069ad305 | |||
| 5bf618aa32 | |||
| 34041a9fe6 | |||
| c6d64aff3a | |||
| 2e5a4e0b71 |
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:
|
||||
- main
|
||||
- staging
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile.*'
|
||||
- '.dockerignore'
|
||||
- '.gitea/workflows/build.yml'
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
build-and-push-branches:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref_type == 'branch'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -24,14 +35,29 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
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
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
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
|
||||
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'
|
||||
|
||||
- name: Build and push Docker image for staging
|
||||
@@ -40,6 +66,87 @@ jobs:
|
||||
context: .
|
||||
push: true
|
||||
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
|
||||
build-args: |
|
||||
VERSION=${{ steps.vars.outputs.VERSION }}
|
||||
BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }}
|
||||
COMMIT=${{ steps.vars.outputs.COMMIT }}
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
|
||||
build-and-push-tags:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.fossy.my.id
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract version and determine release type
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
echo "COMMIT=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
|
||||
if echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
MINOR=$(echo "$VERSION" | cut -d. -f2)
|
||||
|
||||
echo "MAJOR=$MAJOR" >> $GITHUB_OUTPUT
|
||||
echo "MINOR=$MINOR" >> $GITHUB_OUTPUT
|
||||
|
||||
if echo "$VERSION" | grep -q '-'; then
|
||||
echo "IS_PRERELEASE=true" >> $GITHUB_OUTPUT
|
||||
echo "ADDITIONAL_TAG=staging" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "IS_PRERELEASE=false" >> $GITHUB_OUTPUT
|
||||
echo "ADDITIONAL_TAG=latest" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "Invalid version format: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and push Docker image for release
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.VERSION }}
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.MAJOR }}.${{ steps.version.outputs.MINOR }}
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.MAJOR }}
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:${{ steps.version.outputs.ADDITIONAL_TAG }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.VERSION }}
|
||||
BUILD_DATE=${{ steps.version.outputs.BUILD_DATE }}
|
||||
COMMIT=${{ steps.version.outputs.COMMIT }}
|
||||
if: steps.version.outputs.IS_PRERELEASE == 'false'
|
||||
|
||||
- name: Build and push Docker image for pre-release
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.VERSION }}
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:${{ steps.version.outputs.ADDITIONAL_TAG }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.VERSION }}
|
||||
BUILD_DATE=${{ steps.version.outputs.BUILD_DATE }}
|
||||
COMMIT=${{ steps.version.outputs.COMMIT }}
|
||||
if: steps.version.outputs.IS_PRERELEASE == 'true'
|
||||
|
||||
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
|
||||
.env
|
||||
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
|
||||
|
||||
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 . .
|
||||
|
||||
RUN apk update && apk upgrade && apk add --no-cache ca-certificates tzdata
|
||||
RUN update-ca-certificates
|
||||
RUN go build -o ./tmp/main
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
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
|
||||
|
||||
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 /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"]
|
||||
|
||||
265
README.md
265
README.md
@@ -1,93 +1,244 @@
|
||||
# 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
|
||||
- [ ] [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:
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `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 |
|
||||
| `MODE` | Runtime mode: `standalone` (default, no gRPC/auth) or `node` (enable gRPC + auth) | `standalone` | No |
|
||||
| `GRPC_ADDRESS` | gRPC server address/host used in `node` mode | `localhost` | No |
|
||||
| `GRPC_PORT` | gRPC server port used in `node` mode | `8080` | No |
|
||||
| `NODE_TOKEN` | Authentication token sent to controller in `node` mode | - (required in `node`) | Yes (node mode) |
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin http://git.fossy.my.id/bagas/tunnel_pls.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
**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.
|
||||
|
||||
### Automatic TLS Certificate Management
|
||||
|
||||
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/)
|
||||
- [ ] [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)
|
||||
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:
|
||||
|
||||
## 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)
|
||||
- [ ] [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)
|
||||
The buffer pool reuses buffers across connections, preventing memory fragmentation and reducing garbage collection pressure.
|
||||
|
||||
***
|
||||
### 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
|
||||
Choose a self-explaining name for your project.
|
||||
# Analyze memory heap
|
||||
go tool pprof http://localhost:6060/debug/pprof/heap
|
||||
```
|
||||
|
||||
## Description
|
||||
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.
|
||||
## Docker Deployment
|
||||
|
||||
## Badges
|
||||
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.
|
||||
Three Docker Compose configurations are available for different deployment scenarios. Each configuration uses the image `git.fossy.my.id/bagas/tunnel-please:latest`.
|
||||
|
||||
## Visuals
|
||||
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.
|
||||
### Configuration Options
|
||||
|
||||
## Installation
|
||||
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.
|
||||
#### 1. Root with Host Networking (RECOMMENDED)
|
||||
|
||||
## Usage
|
||||
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.
|
||||
**File:** `docker-compose.root.yml`
|
||||
|
||||
## Support
|
||||
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.
|
||||
**Advantages:**
|
||||
- Full TCP port forwarding support (ports 40000-41000)
|
||||
- Direct binding to privileged ports (80, 443, 2200)
|
||||
- Best performance with no NAT overhead
|
||||
- Maximum flexibility for all tunnel types
|
||||
- No port mapping limitations
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
**Use Case:** Production deployments where you need unrestricted TCP forwarding and maximum performance.
|
||||
|
||||
**Deploy:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.root.yml up -d
|
||||
```
|
||||
|
||||
#### 2. Standard (HTTP/HTTPS Only)
|
||||
|
||||
**File:** `docker-compose.standard.yml`
|
||||
|
||||
**Advantages:**
|
||||
- Runs with unprivileged user (more secure)
|
||||
- Standard port mappings (2200, 80, 443)
|
||||
- Simple and predictable networking
|
||||
- TCP port forwarding disabled (`ALLOWED_PORTS=none`)
|
||||
|
||||
**Use Case:** Deployments where you only need HTTP/HTTPS tunneling without custom TCP port forwarding.
|
||||
|
||||
**Deploy:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.standard.yml up -d
|
||||
```
|
||||
|
||||
#### 3. Limited TCP Forwarding
|
||||
|
||||
**File:** `docker-compose.tcp.yml`
|
||||
|
||||
**Advantages:**
|
||||
- Runs with unprivileged user (more secure)
|
||||
- Standard port mappings (2200, 80, 443)
|
||||
- Limited TCP forwarding (ports 30000-31000)
|
||||
- Controlled port range exposure
|
||||
|
||||
**Use Case:** Deployments where you need both HTTP/HTTPS tunneling and limited TCP forwarding within a specific port range.
|
||||
|
||||
**Deploy:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.tcp.yml up -d
|
||||
```
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. **Choose your configuration** based on your requirements
|
||||
2. **Edit the environment variables** in the chosen compose file:
|
||||
- `DOMAIN`: Your domain name (e.g., `example.com`)
|
||||
- `ACME_EMAIL`: Your email for Let's Encrypt
|
||||
- `CF_API_TOKEN`: Your Cloudflare API token (if using automatic TLS)
|
||||
3. **Deploy:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.root.yml up -d
|
||||
```
|
||||
4. **Check logs:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.root.yml logs -f
|
||||
```
|
||||
5. **Stop the service:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.root.yml down
|
||||
```
|
||||
|
||||
### Volume Management
|
||||
|
||||
All configurations use a named volume `certs` for persistent storage:
|
||||
- SSH keys: `/app/certs/ssh/`
|
||||
- TLS certificates: `/app/certs/tls/`
|
||||
|
||||
To backup certificates:
|
||||
```bash
|
||||
docker run --rm -v tunnel_pls_certs:/data -v $(pwd):/backup alpine tar czf /backup/certs-backup.tar.gz -C /data .
|
||||
```
|
||||
|
||||
To restore certificates:
|
||||
```bash
|
||||
docker run --rm -v tunnel_pls_certs:/data -v $(pwd):/backup alpine tar xzf /backup/certs-backup.tar.gz -C /data
|
||||
```
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Use `docker-compose.root.yml`** for production deployments if you need:
|
||||
- Full TCP port forwarding capabilities
|
||||
- Any port range configuration
|
||||
- Direct port binding without mapping overhead
|
||||
- Maximum performance and flexibility
|
||||
|
||||
This is the recommended configuration for most use cases as it provides the complete feature set without limitations.
|
||||
|
||||
## Contributing
|
||||
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.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
1. **Fork** the repository
|
||||
2. Create a new branch for your changes
|
||||
3. Commit and push your updates
|
||||
4. Open a **Pull Request** targeting the **`staging`** branch
|
||||
5. Clearly describe your changes and the reasoning behind them
|
||||
|
||||
## 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
|
||||
|
||||
51
go.mod
51
go.mod
@@ -1,10 +1,55 @@
|
||||
module tunnel_pls
|
||||
|
||||
go 1.24.4
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
git.fossy.my.id/bagas/tunnel-please-grpc v1.2.0
|
||||
github.com/caddyserver/certmagic v0.25.0
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/crypto v0.45.0
|
||||
github.com/libdns/cloudflare v0.2.2
|
||||
github.com/muesli/termenv v0.16.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
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.4.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.3 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.6.2 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // 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.3.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.19 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.4 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // 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.1 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.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.40.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
)
|
||||
|
||||
151
go.sum
151
go.sum
@@ -1,13 +1,142 @@
|
||||
git.fossy.my.id/bagas/tunnel-please-grpc v1.2.0 h1:BS1dJU3wa2ILgTGwkV95Knle0il0OQtErGqyb6xV7SU=
|
||||
git.fossy.my.id/bagas/tunnel-please-grpc v1.2.0/go.mod h1:fG+VkArdkceGB0bNA7IFQus9GetLAwdF5Oi4jdMlXtY=
|
||||
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.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
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.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
|
||||
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||
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.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
|
||||
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
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/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
|
||||
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
|
||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
|
||||
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
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.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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
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.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/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-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
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.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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
|
||||
}
|
||||
376
internal/grpc/client/client.go
Normal file
376
internal/grpc/client/client.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
"tunnel_pls/internal/config"
|
||||
|
||||
"tunnel_pls/session"
|
||||
|
||||
proto "git.fossy.my.id/bagas/tunnel-please-grpc/gen"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type GrpcConfig struct {
|
||||
Address string
|
||||
UseTLS bool
|
||||
InsecureSkipVerify bool
|
||||
Timeout time.Duration
|
||||
KeepAlive bool
|
||||
MaxRetries int
|
||||
KeepAliveTime time.Duration
|
||||
KeepAliveTimeout time.Duration
|
||||
PermitWithoutStream bool
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
conn *grpc.ClientConn
|
||||
config *GrpcConfig
|
||||
sessionRegistry session.Registry
|
||||
slugService proto.SlugChangeClient
|
||||
eventService proto.EventServiceClient
|
||||
authorizeConnectionService proto.UserServiceClient
|
||||
closing bool
|
||||
}
|
||||
|
||||
func DefaultConfig() *GrpcConfig {
|
||||
return &GrpcConfig{
|
||||
Address: "localhost:50051",
|
||||
UseTLS: false,
|
||||
InsecureSkipVerify: false,
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: true,
|
||||
MaxRetries: 3,
|
||||
KeepAliveTime: 2 * time.Minute,
|
||||
KeepAliveTimeout: 10 * time.Second,
|
||||
PermitWithoutStream: false,
|
||||
}
|
||||
}
|
||||
|
||||
func New(config *GrpcConfig, sessionRegistry session.Registry) (*Client, error) {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
} else {
|
||||
defaults := DefaultConfig()
|
||||
if config.Address == "" {
|
||||
config.Address = defaults.Address
|
||||
}
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = defaults.Timeout
|
||||
}
|
||||
if config.MaxRetries == 0 {
|
||||
config.MaxRetries = defaults.MaxRetries
|
||||
}
|
||||
if config.KeepAliveTime == 0 {
|
||||
config.KeepAliveTime = defaults.KeepAliveTime
|
||||
}
|
||||
if config.KeepAliveTimeout == 0 {
|
||||
config.KeepAliveTimeout = defaults.KeepAliveTimeout
|
||||
}
|
||||
}
|
||||
|
||||
var opts []grpc.DialOption
|
||||
|
||||
if config.UseTLS {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: config.InsecureSkipVerify,
|
||||
}
|
||||
creds := credentials.NewTLS(tlsConfig)
|
||||
opts = append(opts, grpc.WithTransportCredentials(creds))
|
||||
} else {
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
}
|
||||
|
||||
if config.KeepAlive {
|
||||
kaParams := keepalive.ClientParameters{
|
||||
Time: config.KeepAliveTime,
|
||||
Timeout: config.KeepAliveTimeout,
|
||||
PermitWithoutStream: config.PermitWithoutStream,
|
||||
}
|
||||
opts = append(opts, grpc.WithKeepaliveParams(kaParams))
|
||||
}
|
||||
|
||||
opts = append(opts,
|
||||
grpc.WithDefaultCallOptions(
|
||||
grpc.MaxCallRecvMsgSize(4*1024*1024),
|
||||
grpc.MaxCallSendMsgSize(4*1024*1024),
|
||||
),
|
||||
)
|
||||
|
||||
conn, err := grpc.NewClient(config.Address, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to gRPC server at %s: %w", config.Address, err)
|
||||
}
|
||||
|
||||
slugService := proto.NewSlugChangeClient(conn)
|
||||
eventService := proto.NewEventServiceClient(conn)
|
||||
authorizeConnectionService := proto.NewUserServiceClient(conn)
|
||||
|
||||
return &Client{
|
||||
conn: conn,
|
||||
config: config,
|
||||
slugService: slugService,
|
||||
sessionRegistry: sessionRegistry,
|
||||
eventService: eventService,
|
||||
authorizeConnectionService: authorizeConnectionService,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) SubscribeEvents(ctx context.Context, identity, authToken string) error {
|
||||
const (
|
||||
baseBackoff = time.Second
|
||||
maxBackoff = 30 * time.Second
|
||||
)
|
||||
|
||||
backoff := baseBackoff
|
||||
wait := func() error {
|
||||
if backoff <= 0 {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
growBackoff := func() {
|
||||
backoff *= 2
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
subscribe, err := c.eventService.Subscribe(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || status.Code(err) == codes.Canceled || ctx.Err() != nil {
|
||||
return err
|
||||
}
|
||||
if !c.isConnectionError(err) || status.Code(err) == codes.Unauthenticated {
|
||||
return err
|
||||
}
|
||||
if err = wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
growBackoff()
|
||||
log.Printf("Reconnect to controller within %v sec", backoff.Seconds())
|
||||
continue
|
||||
}
|
||||
|
||||
err = subscribe.Send(&proto.Node{
|
||||
Type: proto.EventType_AUTHENTICATION,
|
||||
Payload: &proto.Node_AuthEvent{
|
||||
AuthEvent: &proto.Authentication{
|
||||
Identity: identity,
|
||||
AuthToken: authToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Println("Authentication failed to send to gRPC server:", err)
|
||||
if c.isConnectionError(err) {
|
||||
if err = wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
growBackoff()
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Println("Authentication Successfully sent to gRPC server")
|
||||
backoff = baseBackoff
|
||||
|
||||
if err = c.processEventStream(subscribe); err != nil {
|
||||
if errors.Is(err, context.Canceled) || status.Code(err) == codes.Canceled || ctx.Err() != nil {
|
||||
return err
|
||||
}
|
||||
if c.isConnectionError(err) {
|
||||
log.Printf("Reconnect to controller within %v sec", backoff.Seconds())
|
||||
if err = wait(); err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
growBackoff()
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Node, proto.Events]) error {
|
||||
for {
|
||||
recv, err := subscribe.Recv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch recv.GetType() {
|
||||
case proto.EventType_SLUG_CHANGE:
|
||||
oldSlug := recv.GetSlugEvent().GetOld()
|
||||
newSlug := recv.GetSlugEvent().GetNew()
|
||||
sess, err := c.sessionRegistry.Get(oldSlug)
|
||||
if err != nil {
|
||||
errSend := subscribe.Send(&proto.Node{
|
||||
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
||||
Payload: &proto.Node_SlugEventResponse{
|
||||
SlugEventResponse: &proto.SlugChangeEventResponse{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
if errSend != nil {
|
||||
if c.isConnectionError(errSend) {
|
||||
return errSend
|
||||
}
|
||||
log.Printf("non-connection send error for slug change failure: %v", errSend)
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = c.sessionRegistry.Update(oldSlug, newSlug)
|
||||
if err != nil {
|
||||
errSend := subscribe.Send(&proto.Node{
|
||||
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
||||
Payload: &proto.Node_SlugEventResponse{
|
||||
SlugEventResponse: &proto.SlugChangeEventResponse{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
if errSend != nil {
|
||||
if c.isConnectionError(errSend) {
|
||||
return errSend
|
||||
}
|
||||
log.Printf("non-connection send error for slug change failure: %v", errSend)
|
||||
}
|
||||
continue
|
||||
}
|
||||
sess.GetInteraction().Redraw()
|
||||
err = subscribe.Send(&proto.Node{
|
||||
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
||||
Payload: &proto.Node_SlugEventResponse{
|
||||
SlugEventResponse: &proto.SlugChangeEventResponse{
|
||||
Success: true,
|
||||
Message: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if c.isConnectionError(err) {
|
||||
log.Printf("connection error sending slug change success: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Printf("non-connection send error for slug change success: %v", err)
|
||||
continue
|
||||
}
|
||||
case proto.EventType_GET_SESSIONS:
|
||||
sessions := c.sessionRegistry.GetAllSessionFromUser(recv.GetGetSessionsEvent().GetIdentity())
|
||||
var details []*proto.Detail
|
||||
for _, ses := range sessions {
|
||||
detail := ses.Detail()
|
||||
details = append(details, &proto.Detail{
|
||||
Node: config.Getenv("DOMAIN", "localhost"),
|
||||
ForwardingType: detail.ForwardingType,
|
||||
Slug: detail.Slug,
|
||||
UserId: detail.UserID,
|
||||
Active: detail.Active,
|
||||
StartedAt: timestamppb.New(detail.StartedAt),
|
||||
})
|
||||
}
|
||||
err = subscribe.Send(&proto.Node{
|
||||
Type: proto.EventType_GET_SESSIONS,
|
||||
Payload: &proto.Node_GetSessionsEvent{
|
||||
GetSessionsEvent: &proto.GetSessionsResponse{
|
||||
Details: details,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if c.isConnectionError(err) {
|
||||
log.Printf("connection error sending sessions success: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Printf("non-connection send error for sessions success: %v", err)
|
||||
continue
|
||||
}
|
||||
default:
|
||||
log.Printf("Unknown event type received: %v", recv.GetType())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetConnection() *grpc.ClientConn {
|
||||
return c.conn
|
||||
}
|
||||
|
||||
func (c *Client) AuthorizeConn(ctx context.Context, token string) (authorized bool, user string, err error) {
|
||||
check, err := c.authorizeConnectionService.Check(ctx, &proto.CheckRequest{AuthToken: token})
|
||||
if err != nil {
|
||||
return false, "UNAUTHORIZED", err
|
||||
}
|
||||
|
||||
if check.GetResponse() == proto.AuthorizationResponse_MESSAGE_TYPE_UNAUTHORIZED {
|
||||
return false, "UNAUTHORIZED", nil
|
||||
}
|
||||
return true, check.GetUser(), nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.conn != nil {
|
||||
log.Printf("Closing gRPC connection to %s", c.config.Address)
|
||||
c.closing = true
|
||||
return c.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) CheckServerHealth(ctx context.Context) error {
|
||||
healthClient := grpc_health_v1.NewHealthClient(c.GetConnection())
|
||||
resp, err := healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{
|
||||
Service: "",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("health check failed: %w", err)
|
||||
}
|
||||
if resp.Status != grpc_health_v1.HealthCheckResponse_SERVING {
|
||||
return fmt.Errorf("server not serving: %v", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetConfig() *GrpcConfig {
|
||||
return c.config
|
||||
}
|
||||
|
||||
func (c *Client) isConnectionError(err error) bool {
|
||||
if c.closing {
|
||||
return false
|
||||
}
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
return true
|
||||
}
|
||||
switch status.Code(err) {
|
||||
case codes.Unavailable, codes.Canceled, codes.DeadlineExceeded:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
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"
|
||||
"strings"
|
||||
"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
|
||||
ports map[uint16]bool
|
||||
sortedPorts []uint16
|
||||
}
|
||||
|
||||
var Manager = PortManager{
|
||||
var Default Manager = &manager{
|
||||
ports: make(map[uint16]bool),
|
||||
sortedPorts: []uint16{},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rawRange := utils.Getenv("ALLOWED_PORTS")
|
||||
rawRange := config.Getenv("ALLOWED_PORTS", "")
|
||||
if rawRange == "" {
|
||||
return
|
||||
}
|
||||
|
||||
splitRange := strings.Split(rawRange, "-")
|
||||
if len(splitRange) != 2 {
|
||||
Manager.AddPortRange(30000, 31000)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
start, err := strconv.ParseUint(splitRange[0], 10, 16)
|
||||
if err != nil {
|
||||
start = 30000
|
||||
return
|
||||
}
|
||||
end, err := strconv.ParseUint(splitRange[1], 10, 16)
|
||||
if err != nil {
|
||||
end = 31000
|
||||
}
|
||||
Manager.AddPortRange(uint16(start), uint16(end))
|
||||
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()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
@@ -57,7 +68,7 @@ func (pm *PortManager) AddPortRange(startPort, endPort uint16) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *PortManager) GetUnassignedPort() (uint16, bool) {
|
||||
func (pm *manager) GetUnassignedPort() (uint16, bool) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
@@ -70,7 +81,7 @@ func (pm *PortManager) GetUnassignedPort() (uint16, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (pm *PortManager) SetPortStatus(port uint16, assigned bool) error {
|
||||
func (pm *manager) SetPortStatus(port uint16, assigned bool) error {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
@@ -78,7 +89,7 @@ func (pm *PortManager) SetPortStatus(port uint16, assigned bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *PortManager) GetPortStatus(port uint16) (bool, bool) {
|
||||
func (pm *manager) GetPortStatus(port uint16) (bool, bool) {
|
||||
pm.mu.RLock()
|
||||
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()
|
||||
}
|
||||
128
main.go
128
main.go
@@ -1,37 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"tunnel_pls/internal/config"
|
||||
"tunnel_pls/internal/grpc/client"
|
||||
"tunnel_pls/internal/key"
|
||||
"tunnel_pls/server"
|
||||
"tunnel_pls/utils"
|
||||
"tunnel_pls/session"
|
||||
"tunnel_pls/version"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func main() {
|
||||
sshConfig := &ssh.ServerConfig{
|
||||
NoClientAuth: true,
|
||||
ServerVersion: "SSH-2.0-TunnlPls-1.0",
|
||||
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
return nil, nil
|
||||
},
|
||||
if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-v") {
|
||||
fmt.Println(version.GetVersion())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
privateBytes, err := os.ReadFile(utils.Getenv("ssh_private_key"))
|
||||
log.Printf("Starting %s", version.GetVersion())
|
||||
|
||||
mode := strings.ToLower(config.Getenv("MODE", "standalone"))
|
||||
isNodeMode := mode == "node"
|
||||
|
||||
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 {
|
||||
log.Fatalf("Failed to load private key : %s", err.Error())
|
||||
log.Fatalf("Failed to load private key: %s", err)
|
||||
}
|
||||
|
||||
private, err := ssh.ParsePrivateKey(privateBytes)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to parse private key")
|
||||
log.Fatalf("Failed to parse private key: %s", err)
|
||||
}
|
||||
|
||||
sshConfig.AddHostKey(private)
|
||||
app := server.NewServer(*sshConfig)
|
||||
app.Start()
|
||||
sessionRegistry := session.NewRegistry()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
errChan := make(chan error, 2)
|
||||
shutdownChan := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdownChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
var grpcClient *client.Client
|
||||
if isNodeMode {
|
||||
grpcHost := config.Getenv("GRPC_ADDRESS", "localhost")
|
||||
grpcPort := config.Getenv("GRPC_PORT", "8080")
|
||||
grpcAddr := fmt.Sprintf("%s:%s", grpcHost, grpcPort)
|
||||
nodeToken := config.Getenv("NODE_TOKEN", "")
|
||||
if nodeToken == "" {
|
||||
log.Fatalf("NODE_TOKEN is required in node mode")
|
||||
}
|
||||
|
||||
c, err := client.New(&client.GrpcConfig{
|
||||
Address: grpcAddr,
|
||||
UseTLS: false,
|
||||
InsecureSkipVerify: false,
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: true,
|
||||
MaxRetries: 3,
|
||||
}, sessionRegistry)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create grpc client: %v", err)
|
||||
}
|
||||
grpcClient = c
|
||||
|
||||
healthCtx, healthCancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
if err := grpcClient.CheckServerHealth(healthCtx); err != nil {
|
||||
healthCancel()
|
||||
log.Fatalf("gRPC health check failed: %v", err)
|
||||
}
|
||||
healthCancel()
|
||||
|
||||
go func() {
|
||||
identity := config.Getenv("DOMAIN", "localhost")
|
||||
if err := grpcClient.SubscribeEvents(ctx, identity, nodeToken); err != nil {
|
||||
errChan <- fmt.Errorf("failed to subscribe to events: %w", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
app, err := server.NewServer(sshConfig, sessionRegistry, grpcClient)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to start server: %s", err)
|
||||
return
|
||||
}
|
||||
app.Start()
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
log.Printf("error happen : %s", err)
|
||||
case sig := <-shutdownChan:
|
||||
log.Printf("received signal %s, shutting down", sig)
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
if grpcClient != nil {
|
||||
if err := grpcClient.Close(); err != nil {
|
||||
log.Printf("failed to close grpc conn : %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HeaderManager interface {
|
||||
@@ -15,64 +13,169 @@ type HeaderManager interface {
|
||||
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
|
||||
headers map[string]string
|
||||
}
|
||||
|
||||
type RequestHeaderFactory struct {
|
||||
Method string
|
||||
Path string
|
||||
Version string
|
||||
type requestHeaderFactory struct {
|
||||
method string
|
||||
path string
|
||||
version string
|
||||
startLine []byte
|
||||
headers map[string]string
|
||||
}
|
||||
|
||||
func NewRequestHeaderFactory(r io.Reader) (*RequestHeaderFactory, error) {
|
||||
br := bufio.NewReader(r)
|
||||
header := &RequestHeaderFactory{
|
||||
headers: make(map[string]string),
|
||||
func NewRequestHeaderFactory(r interface{}) (RequestHeaderManager, error) {
|
||||
switch v := r.(type) {
|
||||
case []byte:
|
||||
return parseHeadersFromBytes(v)
|
||||
case *bufio.Reader:
|
||||
return parseHeadersFromReader(v)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %T", r)
|
||||
}
|
||||
}
|
||||
|
||||
startLine, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func parseHeadersFromBytes(headerData []byte) (RequestHeaderManager, error) {
|
||||
header := &requestHeaderFactory{
|
||||
headers: make(map[string]string, 16),
|
||||
}
|
||||
startLine = strings.TrimRight(startLine, "\r\n")
|
||||
header.startLine = []byte(startLine)
|
||||
|
||||
parts := strings.Split(startLine, " ")
|
||||
lineEnd := bytes.IndexByte(headerData, '\n')
|
||||
if lineEnd == -1 {
|
||||
return nil, fmt.Errorf("invalid request: no newline found")
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf("invalid request line")
|
||||
}
|
||||
|
||||
header.Method = parts[0]
|
||||
header.Path = parts[1]
|
||||
header.Version = parts[2]
|
||||
header.method = string(parts[0])
|
||||
header.path = string(parts[1])
|
||||
header.version = string(parts[2])
|
||||
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
remaining := headerData[lineEnd+1:]
|
||||
|
||||
for len(remaining) > 0 {
|
||||
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
|
||||
}
|
||||
|
||||
kv := strings.SplitN(line, ":", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
colonIdx := bytes.IndexByte(line, ':')
|
||||
if colonIdx != -1 {
|
||||
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
|
||||
}
|
||||
|
||||
func NewResponseHeaderFactory(startLine []byte) *ResponseHeaderFactory {
|
||||
header := &ResponseHeaderFactory{
|
||||
func parseHeadersFromReader(br *bufio.Reader) (RequestHeaderManager, error) {
|
||||
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,
|
||||
headers: make(map[string]string),
|
||||
}
|
||||
@@ -98,19 +201,19 @@ func NewResponseHeaderFactory(startLine []byte) *ResponseHeaderFactory {
|
||||
return header
|
||||
}
|
||||
|
||||
func (resp *ResponseHeaderFactory) Get(key string) string {
|
||||
func (resp *responseHeaderFactory) Get(key string) string {
|
||||
return resp.headers[key]
|
||||
}
|
||||
|
||||
func (resp *ResponseHeaderFactory) Set(key string, value string) {
|
||||
func (resp *responseHeaderFactory) Set(key string, value string) {
|
||||
resp.headers[key] = value
|
||||
}
|
||||
|
||||
func (resp *ResponseHeaderFactory) Remove(key string) {
|
||||
func (resp *responseHeaderFactory) Remove(key string) {
|
||||
delete(resp.headers, key)
|
||||
}
|
||||
|
||||
func (resp *ResponseHeaderFactory) Finalize() []byte {
|
||||
func (resp *responseHeaderFactory) Finalize() []byte {
|
||||
var buf bytes.Buffer
|
||||
|
||||
buf.Write(resp.startLine)
|
||||
@@ -127,7 +230,7 @@ func (resp *ResponseHeaderFactory) Finalize() []byte {
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (req *RequestHeaderFactory) Get(key string) string {
|
||||
func (req *requestHeaderFactory) Get(key string) string {
|
||||
val, ok := req.headers[key]
|
||||
if !ok {
|
||||
return ""
|
||||
@@ -135,22 +238,32 @@ func (req *RequestHeaderFactory) Get(key string) string {
|
||||
return val
|
||||
}
|
||||
|
||||
func (req *RequestHeaderFactory) Set(key string, value string) {
|
||||
func (req *requestHeaderFactory) Set(key string, value string) {
|
||||
req.headers[key] = value
|
||||
}
|
||||
|
||||
func (req *RequestHeaderFactory) Remove(key string) {
|
||||
func (req *requestHeaderFactory) Remove(key string) {
|
||||
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
|
||||
|
||||
buf.Write(req.startLine)
|
||||
buf.WriteString("\r\n")
|
||||
|
||||
req.headers["X-HF"] = "modified"
|
||||
|
||||
for key, val := range req.headers {
|
||||
buf.WriteString(key)
|
||||
buf.WriteString(": ")
|
||||
|
||||
279
server/http.go
279
server/http.go
@@ -10,40 +10,65 @@ import (
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"tunnel_pls/internal/config"
|
||||
"tunnel_pls/session"
|
||||
"tunnel_pls/session/interaction"
|
||||
"tunnel_pls/utils"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var BadGatewayResponse = []byte("HTTP/1.1 502 Bad Gateway\r\n" +
|
||||
"Content-Length: 11\r\n" +
|
||||
"Content-Type: text/plain\r\n\r\n" +
|
||||
"Bad Gateway")
|
||||
type HTTPWriter interface {
|
||||
io.Reader
|
||||
io.Writer
|
||||
GetRemoteAddr() net.Addr
|
||||
GetWriter() io.Writer
|
||||
AddResponseMiddleware(mw ResponseMiddleware)
|
||||
AddRequestStartMiddleware(mw RequestMiddleware)
|
||||
SetRequestHeader(header RequestHeaderManager)
|
||||
GetRequestStartMiddleware() []RequestMiddleware
|
||||
}
|
||||
|
||||
type CustomWriter struct {
|
||||
RemoteAddr net.Addr
|
||||
type customWriter struct {
|
||||
remoteAddr net.Addr
|
||||
writer io.Writer
|
||||
reader io.Reader
|
||||
headerBuf []byte
|
||||
buf []byte
|
||||
respHeader *ResponseHeaderFactory
|
||||
reqHeader *RequestHeaderFactory
|
||||
interaction interaction.Controller
|
||||
respHeader ResponseHeaderManager
|
||||
reqHeader RequestHeaderManager
|
||||
respMW []ResponseMiddleware
|
||||
reqStartMW []RequestMiddleware
|
||||
reqEndMW []RequestMiddleware
|
||||
}
|
||||
|
||||
func (cw *CustomWriter) SetInteraction(interaction interaction.Controller) {
|
||||
cw.interaction = interaction
|
||||
func (cw *customWriter) GetRemoteAddr() net.Addr {
|
||||
return cw.remoteAddr
|
||||
}
|
||||
|
||||
func (cw *CustomWriter) Read(p []byte) (int, error) {
|
||||
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))
|
||||
read, err := cw.reader.Read(tmp)
|
||||
if err != nil {
|
||||
if read == 0 && err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -52,6 +77,9 @@ func (cw *CustomWriter) Read(p []byte) (int, error) {
|
||||
idx := bytes.Index(tmp, DELIMITER)
|
||||
if idx == -1 {
|
||||
copy(p, tmp)
|
||||
if err != nil {
|
||||
return read, err
|
||||
}
|
||||
return read, nil
|
||||
}
|
||||
|
||||
@@ -64,55 +92,51 @@ func (cw *CustomWriter) Read(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
for _, m := range cw.reqEndMW {
|
||||
err := m.HandleRequest(cw.reqHeader)
|
||||
err = m.HandleRequest(cw.reqHeader)
|
||||
if err != nil {
|
||||
log.Printf("Error when applying request middleware: %v", err)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
headerReader := bufio.NewReader(bytes.NewReader(header))
|
||||
reqhf, err := NewRequestHeaderFactory(headerReader)
|
||||
reqhf, err := NewRequestHeaderFactory(header)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for _, m := range cw.reqStartMW {
|
||||
err := m.HandleRequest(reqhf)
|
||||
if err != nil {
|
||||
log.Printf("Error when applying request middleware: %v", err)
|
||||
return 0, err
|
||||
if mwErr := m.HandleRequest(reqhf); mwErr != nil {
|
||||
log.Printf("Error when applying request middleware: %v", mwErr)
|
||||
return 0, mwErr
|
||||
}
|
||||
}
|
||||
|
||||
cw.reqHeader = reqhf
|
||||
finalHeader := reqhf.Finalize()
|
||||
|
||||
n := copy(p, finalHeader)
|
||||
n += copy(p[n:], body)
|
||||
combined := append(finalHeader, body...)
|
||||
|
||||
n := copy(p, combined)
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func NewCustomWriter(writer io.Writer, reader io.Reader, remoteAddr net.Addr) *CustomWriter {
|
||||
return &CustomWriter{
|
||||
RemoteAddr: remoteAddr,
|
||||
func NewCustomWriter(writer io.Writer, reader io.Reader, remoteAddr net.Addr) HTTPWriter {
|
||||
return &customWriter{
|
||||
remoteAddr: remoteAddr,
|
||||
writer: writer,
|
||||
reader: reader,
|
||||
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 responseLine = regexp.MustCompile(`^HTTP/\d\.\d \d{3} .+`)
|
||||
|
||||
func isHTTPHeader(buf []byte) bool {
|
||||
lines := bytes.Split(buf, []byte("\r\n"))
|
||||
if len(lines) < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
startLine := string(lines[0])
|
||||
if !requestLine.MatchString(startLine) && !responseLine.MatchString(startLine) {
|
||||
return false
|
||||
@@ -122,25 +146,46 @@ func isHTTPHeader(buf []byte) bool {
|
||||
if len(line) == 0 {
|
||||
break
|
||||
}
|
||||
if !bytes.Contains(line, []byte(":")) {
|
||||
colonIdx := bytes.IndexByte(line, ':')
|
||||
if colonIdx <= 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (cw *CustomWriter) Write(p []byte) (int, error) {
|
||||
if len(p) == len(BadGatewayResponse) && bytes.Equal(p, BadGatewayResponse) {
|
||||
return cw.writer.Write(p)
|
||||
func (cw *customWriter) Write(p []byte) (int, error) {
|
||||
if cw.respHeader != nil && len(cw.buf) == 0 && len(p) >= 5 && string(p[0:5]) == "HTTP/" {
|
||||
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...)
|
||||
// TODO: implement middleware buat cache system dll
|
||||
if idx := bytes.Index(cw.buf, DELIMITER); idx != -1 {
|
||||
|
||||
idx := bytes.Index(cw.buf, DELIMITER)
|
||||
if idx == -1 {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
header := cw.buf[:idx+len(DELIMITER)]
|
||||
body := cw.buf[idx+len(DELIMITER):]
|
||||
|
||||
if isHTTPHeader(header) {
|
||||
if !isHTTPHeader(header) {
|
||||
_, err := cw.writer.Write(cw.buf)
|
||||
cw.buf = nil
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
resphf := NewResponseHeaderFactory(header)
|
||||
for _, m := range cw.respMW {
|
||||
err := m.HandleResponse(resphf, body)
|
||||
@@ -151,13 +196,13 @@ func (cw *CustomWriter) Write(p []byte) (int, error) {
|
||||
}
|
||||
header = resphf.Finalize()
|
||||
cw.respHeader = resphf
|
||||
|
||||
_, err := cw.writer.Write(header)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(body) > 0 {
|
||||
_, err := cw.writer.Write(body)
|
||||
_, err = cw.writer.Write(body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -165,40 +210,36 @@ func (cw *CustomWriter) Write(p []byte) (int, error) {
|
||||
cw.buf = nil
|
||||
return len(p), nil
|
||||
}
|
||||
}
|
||||
|
||||
cw.buf = nil
|
||||
n, err := cw.writer.Write(p)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
for _, m := range cw.respMW {
|
||||
err := m.HandleResponse(cw.respHeader, p)
|
||||
if err != nil {
|
||||
log.Printf("Cannot apply middleware: %s\n", err)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (cw *CustomWriter) AddInteraction(interaction *interaction.Interaction) {
|
||||
cw.interaction = interaction
|
||||
}
|
||||
|
||||
var redirectTLS = false
|
||||
|
||||
func NewHTTPServer() error {
|
||||
listener, err := net.Listen("tcp", ":80")
|
||||
type HTTPServer interface {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
var conn net.Conn
|
||||
conn, err = listener.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
@@ -207,16 +248,16 @@ func NewHTTPServer() error {
|
||||
continue
|
||||
}
|
||||
|
||||
go Handler(conn)
|
||||
go hs.handler(conn)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func Handler(conn net.Conn) {
|
||||
func (hs *httpServer) handler(conn net.Conn) {
|
||||
defer func() {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
log.Printf("Error closing connection: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -243,8 +284,8 @@ func Handler(conn net.Conn) {
|
||||
slug := host[0]
|
||||
|
||||
if redirectTLS {
|
||||
_, err := conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
|
||||
fmt.Sprintf("Location: https://%s.%s/\r\n", slug, utils.Getenv("domain")) +
|
||||
_, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
|
||||
fmt.Sprintf("Location: https://%s.%s/\r\n", slug, config.Getenv("DOMAIN", "localhost")) +
|
||||
"Content-Length: 0\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
"\r\n"))
|
||||
@@ -256,8 +297,7 @@ func Handler(conn net.Conn) {
|
||||
}
|
||||
|
||||
if slug == "ping" {
|
||||
// TODO: implement cors
|
||||
_, err := conn.Write([]byte(
|
||||
_, err = conn.Write([]byte(
|
||||
"HTTP/1.1 200 OK\r\n" +
|
||||
"Content-Length: 0\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
@@ -273,9 +313,9 @@ func Handler(conn net.Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
sshSession, ok := session.Clients[slug]
|
||||
if !ok {
|
||||
_, err := conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
|
||||
sshSession, err := hs.sessionRegistry.Get(slug)
|
||||
if err != nil {
|
||||
_, 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) +
|
||||
"Content-Length: 0\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
@@ -284,79 +324,68 @@ func Handler(conn net.Conn) {
|
||||
log.Println("Failed to write 301 Moved Permanently:", err)
|
||||
return
|
||||
}
|
||||
err = conn.Close()
|
||||
if err != nil {
|
||||
log.Println("Failed to close connection:", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
cw := NewCustomWriter(conn, dstReader, conn.RemoteAddr())
|
||||
cw.SetInteraction(sshSession.Interaction)
|
||||
forwardRequest(cw, reqhf, sshSession)
|
||||
return
|
||||
}
|
||||
|
||||
func forwardRequest(cw *CustomWriter, initialRequest *RequestHeaderFactory, sshSession *session.SSHSession) {
|
||||
payload := sshSession.Forwarder.CreateForwardedTCPIPPayload(cw.RemoteAddr)
|
||||
channel, reqs, err := sshSession.Lifecycle.GetConnection().OpenChannel("forwarded-tcpip", payload)
|
||||
if err != nil {
|
||||
log.Printf("Failed to open forwarded-tcpip channel: %v", err)
|
||||
sendBadGatewayResponse(cw)
|
||||
return
|
||||
func forwardRequest(cw HTTPWriter, initialRequest RequestHeaderManager, sshSession *session.SSHSession) {
|
||||
payload := sshSession.GetForwarder().CreateForwardedTCPIPPayload(cw.GetRemoteAddr())
|
||||
|
||||
type channelResult struct {
|
||||
channel ssh.Channel
|
||||
reqs <-chan *ssh.Request
|
||||
err error
|
||||
}
|
||||
defer func(channel ssh.Channel) {
|
||||
err := channel.Close()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
sendBadGatewayResponse(cw)
|
||||
return
|
||||
}
|
||||
log.Println("Failed to close connection:", err)
|
||||
return
|
||||
}
|
||||
}(channel)
|
||||
resultChan := make(chan channelResult, 1)
|
||||
|
||||
go func() {
|
||||
for req := range reqs {
|
||||
err := req.Reply(false, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to reply to request: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
channel, reqs, err := sshSession.GetLifecycle().GetConnection().OpenChannel("forwarded-tcpip", payload)
|
||||
resultChan <- channelResult{channel, reqs, err}
|
||||
}()
|
||||
_, err = channel.Write(initialRequest.Finalize())
|
||||
if err != nil {
|
||||
log.Printf("Failed to forward request: %v", err)
|
||||
|
||||
var channel ssh.Channel
|
||||
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
|
||||
}
|
||||
//TODO: Implement wrapper func buat add/remove middleware
|
||||
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
|
||||
}
|
||||
|
||||
go ssh.DiscardRequests(reqs)
|
||||
|
||||
fingerprintMiddleware := NewTunnelFingerprint()
|
||||
loggerMiddleware := NewRequestLogger(cw.interaction, cw.RemoteAddr)
|
||||
cw.respMW = append(cw.respMW, fingerprintMiddleware)
|
||||
cw.reqStartMW = append(cw.reqStartMW, loggerMiddleware)
|
||||
forwardedForMiddleware := NewForwardedFor(cw.GetRemoteAddr())
|
||||
|
||||
//TODO: Tambah req Middleware
|
||||
cw.reqEndMW = nil
|
||||
cw.reqHeader = initialRequest
|
||||
cw.AddResponseMiddleware(fingerprintMiddleware)
|
||||
cw.AddRequestStartMiddleware(forwardedForMiddleware)
|
||||
cw.SetRequestHeader(initialRequest)
|
||||
|
||||
for _, m := range cw.reqStartMW {
|
||||
err := m.HandleRequest(cw.reqHeader)
|
||||
if err != nil {
|
||||
for _, m := range cw.GetRequestStartMiddleware() {
|
||||
if err := m.HandleRequest(initialRequest); err != nil {
|
||||
log.Printf("Error handling request: %v", err)
|
||||
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
|
||||
}
|
||||
|
||||
func sendBadGatewayResponse(writer io.Writer) {
|
||||
_, err := writer.Write(BadGatewayResponse)
|
||||
if err != nil {
|
||||
log.Printf("failed to write Bad Gateway response: %v", err)
|
||||
sshSession.GetForwarder().HandleConnection(cw, channel, cw.GetRemoteAddr())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,25 +8,27 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"tunnel_pls/session"
|
||||
"tunnel_pls/utils"
|
||||
"tunnel_pls/internal/config"
|
||||
)
|
||||
|
||||
func NewHTTPSServer() error {
|
||||
cert, err := tls.LoadX509KeyPair(utils.Getenv("cert_loc"), utils.Getenv("key_loc"))
|
||||
func (hs *httpServer) ListenAndServeTLS() error {
|
||||
domain := config.Getenv("DOMAIN", "localhost")
|
||||
httpsPort := config.Getenv("HTTPS_PORT", "8443")
|
||||
|
||||
tlsConfig, err := NewTLSConfig(domain)
|
||||
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", ":443", config)
|
||||
ln, err := tls.Listen("tcp", ":"+httpsPort, tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
var conn net.Conn
|
||||
conn, err = ln.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
log.Println("https server closed")
|
||||
@@ -35,13 +37,13 @@ func NewHTTPSServer() error {
|
||||
continue
|
||||
}
|
||||
|
||||
go HandlerTLS(conn)
|
||||
go hs.handlerTLS(conn)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandlerTLS(conn net.Conn) {
|
||||
func (hs *httpServer) handlerTLS(conn net.Conn) {
|
||||
defer func() {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
@@ -60,24 +62,18 @@ func HandlerTLS(conn net.Conn) {
|
||||
|
||||
host := strings.Split(reqhf.Get("Host"), ".")
|
||||
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 {
|
||||
log.Println("Failed to write 400 Bad Request:", err)
|
||||
return
|
||||
}
|
||||
err = conn.Close()
|
||||
if err != nil {
|
||||
log.Println("Failed to close connection:", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
slug := host[0]
|
||||
|
||||
if slug == "ping" {
|
||||
// TODO: implement cors
|
||||
_, err := conn.Write([]byte(
|
||||
_, err = conn.Write([]byte(
|
||||
"HTTP/1.1 200 OK\r\n" +
|
||||
"Content-Length: 0\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
@@ -93,9 +89,9 @@ func HandlerTLS(conn net.Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
sshSession, ok := session.Clients[slug]
|
||||
if !ok {
|
||||
_, err := conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
|
||||
sshSession, err := hs.sessionRegistry.Get(slug)
|
||||
if err != nil {
|
||||
_, 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) +
|
||||
"Content-Length: 0\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
@@ -104,15 +100,9 @@ func HandlerTLS(conn net.Conn) {
|
||||
log.Println("Failed to write 301 Moved Permanently:", err)
|
||||
return
|
||||
}
|
||||
err = conn.Close()
|
||||
if err != nil {
|
||||
log.Println("Failed to close connection:", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
cw := NewCustomWriter(conn, dstReader, conn.RemoteAddr())
|
||||
|
||||
forwardRequest(cw, reqhf, sshSession)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
"tunnel_pls/session/interaction"
|
||||
)
|
||||
|
||||
type RequestMiddleware interface {
|
||||
HandleRequest(header *RequestHeaderFactory) error
|
||||
HandleRequest(header RequestHeaderManager) error
|
||||
}
|
||||
|
||||
type ResponseMiddleware interface {
|
||||
HandleResponse(header *ResponseHeaderFactory, body []byte) error
|
||||
HandleResponse(header ResponseHeaderManager, body []byte) error
|
||||
}
|
||||
|
||||
type TunnelFingerprint struct{}
|
||||
@@ -20,143 +17,25 @@ type TunnelFingerprint struct{}
|
||||
func NewTunnelFingerprint() *TunnelFingerprint {
|
||||
return &TunnelFingerprint{}
|
||||
}
|
||||
func (h *TunnelFingerprint) HandleRequest(header *RequestHeaderFactory) error {
|
||||
return nil
|
||||
}
|
||||
func (h *TunnelFingerprint) HandleResponse(header *ResponseHeaderFactory, body []byte) error {
|
||||
|
||||
func (h *TunnelFingerprint) HandleResponse(header ResponseHeaderManager, body []byte) error {
|
||||
header.Set("Server", "Tunnel Please")
|
||||
return nil
|
||||
}
|
||||
|
||||
type RequestLogger struct {
|
||||
interaction interaction.Controller
|
||||
remoteAddr net.Addr
|
||||
type ForwardedFor struct {
|
||||
addr net.Addr
|
||||
}
|
||||
|
||||
func NewRequestLogger(interaction interaction.Controller, remoteAddr net.Addr) *RequestLogger {
|
||||
return &RequestLogger{
|
||||
interaction: interaction,
|
||||
remoteAddr: remoteAddr,
|
||||
func NewForwardedFor(addr net.Addr) *ForwardedFor {
|
||||
return &ForwardedFor{addr: addr}
|
||||
}
|
||||
|
||||
func (ff *ForwardedFor) HandleRequest(header RequestHeaderManager) error {
|
||||
host, _, err := net.SplitHostPort(ff.addr.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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))
|
||||
header.Set("X-Forwarded-For", host)
|
||||
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
|
||||
//}
|
||||
|
||||
@@ -1,52 +1,58 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"tunnel_pls/utils"
|
||||
"tunnel_pls/internal/config"
|
||||
"tunnel_pls/internal/grpc/client"
|
||||
"tunnel_pls/session"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Conn *net.Listener
|
||||
Config *ssh.ServerConfig
|
||||
HttpServer *http.Server
|
||||
conn *net.Listener
|
||||
config *ssh.ServerConfig
|
||||
sessionRegistry session.Registry
|
||||
grpcClient *client.Client
|
||||
}
|
||||
|
||||
func NewServer(config ssh.ServerConfig) *Server {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%s", utils.Getenv("port")))
|
||||
func NewServer(sshConfig *ssh.ServerConfig, sessionRegistry session.Registry, grpcClient *client.Client) (*Server, error) {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%s", config.Getenv("PORT", "2200")))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen on port 2200: %v", err)
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
if utils.Getenv("tls_enabled") == "true" {
|
||||
go func() {
|
||||
err := NewHTTPSServer()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to start https server: %v", err)
|
||||
}
|
||||
return
|
||||
}()
|
||||
}
|
||||
go func() {
|
||||
err := NewHTTPServer()
|
||||
|
||||
HttpServer := NewHTTPServer(sessionRegistry)
|
||||
err = HttpServer.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to start http server: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}()
|
||||
|
||||
if config.Getenv("TLS_ENABLED", "false") == "true" {
|
||||
err = HttpServer.ListenAndServeTLS()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to start https server: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &Server{
|
||||
Conn: &listener,
|
||||
Config: &config,
|
||||
}
|
||||
conn: &listener,
|
||||
config: sshConfig,
|
||||
sessionRegistry: sessionRegistry,
|
||||
grpcClient: grpcClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() {
|
||||
log.Println("SSH server is starting on port 2200...")
|
||||
for {
|
||||
conn, err := (*s.Conn).Accept()
|
||||
conn, err := (*s.conn).Accept()
|
||||
if err != nil {
|
||||
log.Printf("failed to accept connection: %v", err)
|
||||
continue
|
||||
@@ -55,3 +61,39 @@ func (s *Server) Start() {
|
||||
go s.handleConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleConnection(conn net.Conn) {
|
||||
sshConn, chans, forwardingReqs, err := ssh.NewServerConn(conn, s.config)
|
||||
defer func(sshConn *ssh.ServerConn) {
|
||||
err = sshConn.Close()
|
||||
if err != nil {
|
||||
log.Printf("failed to close SSH server: %v", err)
|
||||
}
|
||||
}(sshConn)
|
||||
|
||||
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
|
||||
}
|
||||
ctx := context.Background()
|
||||
log.Println("SSH connection established:", sshConn.User())
|
||||
|
||||
user := "UNAUTHORIZED"
|
||||
if s.grpcClient != nil {
|
||||
_, u, _ := s.grpcClient.AuthorizeConn(ctx, sshConn.User())
|
||||
user = u
|
||||
}
|
||||
|
||||
sshSession := session.New(sshConn, forwardingReqs, chans, s.sessionRegistry, user)
|
||||
err = sshSession.Start()
|
||||
if err != nil {
|
||||
log.Printf("SSH session ended with error: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
336
server/tls.go
Normal file
336
server/tls.go
Normal file
@@ -0,0 +1,336 @@
|
||||
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.VersionTLS13,
|
||||
MaxVersion: tls.VersionTLS13,
|
||||
|
||||
SessionTicketsDisabled: false,
|
||||
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
},
|
||||
|
||||
CurvePreferences: []tls.CurveID{
|
||||
tls.X25519,
|
||||
},
|
||||
|
||||
ClientAuth: tls.NoClientCert,
|
||||
NextProtos: nil,
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
"tunnel_pls/internal/config"
|
||||
"tunnel_pls/session/slug"
|
||||
"tunnel_pls/types"
|
||||
|
||||
"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 {
|
||||
Listener net.Listener
|
||||
TunnelType types.TunnelType
|
||||
ForwardedPort uint16
|
||||
SlugManager slug.Manager
|
||||
Lifecycle Lifecycle
|
||||
listener net.Listener
|
||||
tunnelType types.TunnelType
|
||||
forwardedPort uint16
|
||||
slugManager slug.Manager
|
||||
lifecycle Lifecycle
|
||||
}
|
||||
|
||||
func NewForwarder(slugManager slug.Manager) *Forwarder {
|
||||
return &Forwarder{
|
||||
listener: nil,
|
||||
tunnelType: "",
|
||||
forwardedPort: 0,
|
||||
slugManager: slugManager,
|
||||
lifecycle: nil,
|
||||
}
|
||||
}
|
||||
|
||||
type Lifecycle interface {
|
||||
@@ -38,10 +64,11 @@ type ForwardingController interface {
|
||||
HandleConnection(dst io.ReadWriter, src ssh.Channel, remoteAddr net.Addr)
|
||||
SetLifecycle(lifecycle Lifecycle)
|
||||
CreateForwardedTCPIPPayload(origin net.Addr) []byte
|
||||
WriteBadGatewayResponse(dst io.Writer)
|
||||
}
|
||||
|
||||
func (f *Forwarder) SetLifecycle(lifecycle Lifecycle) {
|
||||
f.Lifecycle = lifecycle
|
||||
f.lifecycle = lifecycle
|
||||
}
|
||||
|
||||
func (f *Forwarder) AcceptTCPConnections() {
|
||||
@@ -54,77 +81,134 @@ func (f *Forwarder) AcceptTCPConnections() {
|
||||
log.Printf("Error accepting connection: %v", err)
|
||||
continue
|
||||
}
|
||||
payload := f.CreateForwardedTCPIPPayload(conn.RemoteAddr())
|
||||
channel, reqs, err := f.Lifecycle.GetConnection().OpenChannel("forwarded-tcpip", payload)
|
||||
if err != nil {
|
||||
log.Printf("Failed to open forwarded-tcpip channel: %v", err)
|
||||
return
|
||||
|
||||
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||
log.Printf("Failed to set connection deadline: %v", err)
|
||||
if closeErr := conn.Close(); closeErr != nil {
|
||||
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() {
|
||||
for req := range reqs {
|
||||
err := req.Reply(false, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to reply to request: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
channel, reqs, err := f.lifecycle.GetConnection().OpenChannel("forwarded-tcpip", payload)
|
||||
resultChan <- channelResult{channel, reqs, err}
|
||||
}()
|
||||
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) {
|
||||
defer func(src ssh.Channel) {
|
||||
err := src.Close()
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Printf("Error closing connection: %v", err)
|
||||
defer func() {
|
||||
_, err := io.Copy(io.Discard, src)
|
||||
if err != nil {
|
||||
log.Printf("Failed to discard connection: %v", err)
|
||||
}
|
||||
}(src)
|
||||
log.Printf("Handling new forwarded connection from %s", remoteAddr)
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(src, dst)
|
||||
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
|
||||
log.Printf("Error copying from conn.Reader to channel: %v", err)
|
||||
err = src.Close()
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Printf("Error closing source channel: %v", err)
|
||||
}
|
||||
|
||||
if closer, ok := dst.(io.Closer); ok {
|
||||
err = closer.Close()
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
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) {
|
||||
log.Printf("Error copying from channel to conn.Writer: %v", err)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
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)
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
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) {
|
||||
f.TunnelType = tunnelType
|
||||
f.tunnelType = tunnelType
|
||||
}
|
||||
|
||||
func (f *Forwarder) GetTunnelType() types.TunnelType {
|
||||
return f.TunnelType
|
||||
return f.tunnelType
|
||||
}
|
||||
|
||||
func (f *Forwarder) GetForwardedPort() uint16 {
|
||||
return f.ForwardedPort
|
||||
return f.forwardedPort
|
||||
}
|
||||
|
||||
func (f *Forwarder) SetForwardedPort(port uint16) {
|
||||
f.ForwardedPort = port
|
||||
f.forwardedPort = port
|
||||
}
|
||||
|
||||
func (f *Forwarder) SetListener(listener net.Listener) {
|
||||
f.Listener = listener
|
||||
f.listener = listener
|
||||
}
|
||||
|
||||
func (f *Forwarder) GetListener() net.Listener {
|
||||
return f.Listener
|
||||
return f.listener
|
||||
}
|
||||
|
||||
func (f *Forwarder) WriteBadGatewayResponse(dst io.Writer) {
|
||||
_, err := dst.Write(types.BadGatewayResponse)
|
||||
if err != nil {
|
||||
log.Printf("failed to write Bad Gateway response: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Forwarder) Close() error {
|
||||
if f.GetTunnelType() != types.HTTP {
|
||||
return f.Listener.Close()
|
||||
if f.GetListener() != nil {
|
||||
return f.listener.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
portUtil "tunnel_pls/internal/port"
|
||||
"tunnel_pls/internal/random"
|
||||
"tunnel_pls/types"
|
||||
|
||||
"tunnel_pls/utils"
|
||||
|
||||
"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) {
|
||||
for req := range GlobalRequest {
|
||||
switch req.Type {
|
||||
case "tcpip-forward":
|
||||
s.HandleTCPIPForward(req)
|
||||
case "shell", "pty-req":
|
||||
err := req.Reply(true, nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
return
|
||||
case "shell", "pty-req", "window-change":
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
return
|
||||
}
|
||||
err = s.Lifecycle.Close()
|
||||
err = s.lifecycle.Close()
|
||||
if err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
@@ -62,13 +79,12 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
|
||||
var rawPortToBind uint32
|
||||
if err := binary.Read(reader, binary.BigEndian, &rawPortToBind); err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
return
|
||||
}
|
||||
err = s.Lifecycle.Close()
|
||||
err = s.lifecycle.Close()
|
||||
if err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
@@ -76,13 +92,13 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
return
|
||||
}
|
||||
err = s.Lifecycle.Close()
|
||||
err = s.lifecycle.Close()
|
||||
if err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
@@ -90,163 +106,161 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
|
||||
}
|
||||
|
||||
portToBind := uint16(rawPortToBind)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
return
|
||||
}
|
||||
err = s.Lifecycle.Close()
|
||||
err = s.lifecycle.Close()
|
||||
if err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s.Interaction.SendMessage("\033[H\033[2J")
|
||||
s.Lifecycle.SetStatus(types.RUNNING)
|
||||
go s.Interaction.HandleUserInput()
|
||||
|
||||
if portToBind == 80 || portToBind == 443 {
|
||||
s.HandleHTTPForward(req, portToBind)
|
||||
return
|
||||
} else {
|
||||
}
|
||||
if portToBind == 0 {
|
||||
unassign, success := portUtil.Manager.GetUnassignedPort()
|
||||
unassign, success := portUtil.Default.GetUnassignedPort()
|
||||
portToBind = unassign
|
||||
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()
|
||||
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))
|
||||
} else if isUse, isExist := portUtil.Default.GetPortStatus(portToBind); isExist && isUse {
|
||||
log.Printf("Port %d is already in use or restricted", portToBind)
|
||||
err := req.Reply(false, nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
return
|
||||
}
|
||||
err = s.Lifecycle.Close()
|
||||
err = s.lifecycle.Close()
|
||||
if err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err := portUtil.Manager.SetPortStatus(portToBind, true)
|
||||
err = portUtil.Default.SetPortStatus(portToBind, true)
|
||||
if err != nil {
|
||||
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 := random.GenerateRandomString(20)
|
||||
|
||||
slug := generateUniqueSlug()
|
||||
if slug == "" {
|
||||
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
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s.SlugManager.Set(slug)
|
||||
registerClient(slug, s)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err := binary.Write(buf, binary.BigEndian, uint32(80))
|
||||
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", 80)
|
||||
log.Printf("HTTP forwarding approved on port: %d", portToBind)
|
||||
|
||||
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)
|
||||
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) {
|
||||
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)
|
||||
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()
|
||||
err = s.lifecycle.Close()
|
||||
if err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
s.Forwarder.SetListener(listener)
|
||||
s.Forwarder.SetForwardedPort(portToBind)
|
||||
s.Interaction.ShowWelcomeMessage()
|
||||
s.Interaction.SendMessage(fmt.Sprintf("Forwarding your traffic to tcp://%s:%d \r\n", utils.Getenv("domain"), s.Forwarder.GetForwardedPort()))
|
||||
|
||||
go s.Forwarder.AcceptTCPConnections()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err = binary.Write(buf, binary.BigEndian, uint32(portToBind))
|
||||
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
|
||||
}
|
||||
|
||||
log.Printf("TCP forwarding approved on port: %d", portToBind)
|
||||
err = req.Reply(true, buf.Bytes())
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func generateUniqueSlug() string {
|
||||
maxAttempts := 5
|
||||
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
slug := utils.GenerateRandomString(20)
|
||||
|
||||
clientsMutex.RLock()
|
||||
_, exists := Clients[slug]
|
||||
clientsMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return slug
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Failed to generate unique slug after multiple attempts")
|
||||
return ""
|
||||
s.forwarder.SetType(types.TCP)
|
||||
s.forwarder.SetListener(listener)
|
||||
s.forwarder.SetForwardedPort(portToBind)
|
||||
s.lifecycle.SetStatus(types.RUNNING)
|
||||
go s.forwarder.AcceptTCPConnections()
|
||||
s.interaction.Start()
|
||||
}
|
||||
|
||||
func readSSHString(reader *bytes.Reader) (string, error) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,10 @@ package lifecycle
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
portUtil "tunnel_pls/internal/port"
|
||||
"tunnel_pls/session/slug"
|
||||
"tunnel_pls/types"
|
||||
@@ -14,10 +13,6 @@ import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Interaction interface {
|
||||
SendMessage(string)
|
||||
}
|
||||
|
||||
type Forwarder interface {
|
||||
Close() error
|
||||
GetTunnelType() types.TunnelType
|
||||
@@ -25,14 +20,25 @@ type Forwarder interface {
|
||||
}
|
||||
|
||||
type Lifecycle struct {
|
||||
Status types.Status
|
||||
Conn ssh.Conn
|
||||
Channel ssh.Channel
|
||||
|
||||
Interaction Interaction
|
||||
Forwarder Forwarder
|
||||
SlugManager slug.Manager
|
||||
status types.Status
|
||||
conn ssh.Conn
|
||||
channel ssh.Channel
|
||||
forwarder Forwarder
|
||||
slugManager slug.Manager
|
||||
unregisterClient func(slug string)
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
func NewLifecycle(conn ssh.Conn, forwarder Forwarder, slugManager slug.Manager) *Lifecycle {
|
||||
return &Lifecycle{
|
||||
status: types.INITIALIZING,
|
||||
conn: conn,
|
||||
channel: nil,
|
||||
forwarder: forwarder,
|
||||
slugManager: slugManager,
|
||||
unregisterClient: nil,
|
||||
startedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lifecycle) SetUnregisterClient(unregisterClient func(slug string)) {
|
||||
@@ -41,82 +47,59 @@ func (l *Lifecycle) SetUnregisterClient(unregisterClient func(slug string)) {
|
||||
|
||||
type SessionLifecycle interface {
|
||||
Close() error
|
||||
WaitForRunningStatus()
|
||||
SetStatus(status types.Status)
|
||||
GetConnection() ssh.Conn
|
||||
GetChannel() ssh.Channel
|
||||
SetChannel(channel ssh.Channel)
|
||||
SetUnregisterClient(unregisterClient func(slug string))
|
||||
IsActive() bool
|
||||
StartedAt() time.Time
|
||||
}
|
||||
|
||||
func (l *Lifecycle) GetChannel() ssh.Channel {
|
||||
return l.Channel
|
||||
return l.channel
|
||||
}
|
||||
|
||||
func (l *Lifecycle) SetChannel(channel ssh.Channel) {
|
||||
l.Channel = channel
|
||||
l.channel = channel
|
||||
}
|
||||
func (l *Lifecycle) GetConnection() ssh.Conn {
|
||||
return l.Conn
|
||||
return l.conn
|
||||
}
|
||||
func (l *Lifecycle) SetStatus(status types.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
|
||||
}
|
||||
l.status = status
|
||||
if status == types.RUNNING && l.startedAt.IsZero() {
|
||||
l.startedAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lifecycle) Close() error {
|
||||
err := l.Forwarder.Close()
|
||||
err := l.forwarder.Close()
|
||||
if err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
return err
|
||||
}
|
||||
|
||||
if l.Channel != nil {
|
||||
err := l.Channel.Close()
|
||||
if l.channel != nil {
|
||||
err := l.channel.Close()
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if l.Conn != nil {
|
||||
err := l.Conn.Close()
|
||||
if l.conn != nil {
|
||||
err := l.conn.Close()
|
||||
if err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
clientSlug := l.SlugManager.Get()
|
||||
clientSlug := l.slugManager.Get()
|
||||
if clientSlug != "" {
|
||||
l.unregisterClient(clientSlug)
|
||||
}
|
||||
|
||||
if l.Forwarder.GetTunnelType() == types.TCP {
|
||||
err := portUtil.Manager.SetPortStatus(l.Forwarder.GetForwardedPort(), false)
|
||||
if l.forwarder.GetTunnelType() == types.TCP {
|
||||
err := portUtil.Default.SetPortStatus(l.forwarder.GetForwardedPort(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -124,3 +107,11 @@ func (l *Lifecycle) Close() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Lifecycle) IsActive() bool {
|
||||
return l.status == types.RUNNING
|
||||
}
|
||||
|
||||
func (l *Lifecycle) StartedAt() time.Time {
|
||||
return l.startedAt
|
||||
}
|
||||
|
||||
290
session/registry.go
Normal file
290
session/registry.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Registry interface {
|
||||
Get(slug string) (session *SSHSession, err error)
|
||||
Update(oldSlug, newSlug string) error
|
||||
Register(slug string, session *SSHSession) (success bool)
|
||||
Remove(slug string)
|
||||
GetAllSessionFromUser(user string) []*SSHSession
|
||||
}
|
||||
type registry struct {
|
||||
mu sync.RWMutex
|
||||
byUser map[string]map[string]*SSHSession
|
||||
slugIndex map[string]string
|
||||
}
|
||||
|
||||
func NewRegistry() Registry {
|
||||
return ®istry{
|
||||
byUser: make(map[string]map[string]*SSHSession),
|
||||
slugIndex: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registry) Get(slug string) (session *SSHSession, err error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
userID, ok := r.slugIndex[slug]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("session not found")
|
||||
}
|
||||
|
||||
client, ok := r.byUser[userID][slug]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("session not found")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (r *registry) Update(oldSlug, newSlug string) error {
|
||||
if isForbiddenSlug(newSlug) {
|
||||
return fmt.Errorf("this subdomain is reserved. Please choose a different one")
|
||||
} else if !isValidSlug(newSlug) {
|
||||
return fmt.Errorf("invalid subdomain. Follow the rules")
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
userID, ok := r.slugIndex[oldSlug]
|
||||
if !ok {
|
||||
return fmt.Errorf("session not found")
|
||||
}
|
||||
|
||||
if _, exists := r.slugIndex[newSlug]; exists && newSlug != oldSlug {
|
||||
return fmt.Errorf("someone already uses this subdomain")
|
||||
}
|
||||
|
||||
client, ok := r.byUser[userID][oldSlug]
|
||||
if !ok {
|
||||
return fmt.Errorf("session not found")
|
||||
}
|
||||
|
||||
delete(r.byUser[userID], oldSlug)
|
||||
delete(r.slugIndex, oldSlug)
|
||||
|
||||
client.slugManager.Set(newSlug)
|
||||
r.slugIndex[newSlug] = userID
|
||||
|
||||
if r.byUser[userID] == nil {
|
||||
r.byUser[userID] = make(map[string]*SSHSession)
|
||||
}
|
||||
r.byUser[userID][newSlug] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registry) Register(slug string, session *SSHSession) (success bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, exists := r.slugIndex[slug]; exists {
|
||||
return false
|
||||
}
|
||||
|
||||
userID := session.userID
|
||||
if r.byUser[userID] == nil {
|
||||
r.byUser[userID] = make(map[string]*SSHSession)
|
||||
}
|
||||
|
||||
r.byUser[userID][slug] = session
|
||||
r.slugIndex[slug] = userID
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *registry) GetAllSessionFromUser(user string) []*SSHSession {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
m := r.byUser[user]
|
||||
if len(m) == 0 {
|
||||
return []*SSHSession{}
|
||||
}
|
||||
|
||||
sessions := make([]*SSHSession, 0, len(m))
|
||||
for _, s := range m {
|
||||
sessions = append(sessions, s)
|
||||
}
|
||||
return sessions
|
||||
}
|
||||
|
||||
func (r *registry) Remove(slug string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
userID, ok := r.slugIndex[slug]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
delete(r.byUser[userID], slug)
|
||||
if len(r.byUser[userID]) == 0 {
|
||||
delete(r.byUser, userID)
|
||||
}
|
||||
delete(r.slugIndex, slug)
|
||||
}
|
||||
|
||||
func isValidSlug(slug string) bool {
|
||||
if len(slug) < minSlugLength || len(slug) > maxSlugLength {
|
||||
return false
|
||||
}
|
||||
|
||||
if slug[0] == '-' || slug[len(slug)-1] == '-' {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range slug {
|
||||
if !isValidSlugChar(byte(c)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isValidSlugChar(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-'
|
||||
}
|
||||
|
||||
func isForbiddenSlug(slug string) bool {
|
||||
_, ok := forbiddenSlugs[slug]
|
||||
return ok
|
||||
}
|
||||
|
||||
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": {},
|
||||
}
|
||||
|
||||
var (
|
||||
minSlugLength = 3
|
||||
maxSlugLength = 20
|
||||
)
|
||||
@@ -1,23 +1,18 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
"tunnel_pls/internal/config"
|
||||
"tunnel_pls/session/forwarder"
|
||||
"tunnel_pls/session/interaction"
|
||||
"tunnel_pls/session/lifecycle"
|
||||
"tunnel_pls/session/slug"
|
||||
"tunnel_pls/types"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
clientsMutex sync.RWMutex
|
||||
Clients = make(map[string]*SSHSession)
|
||||
)
|
||||
|
||||
type Session interface {
|
||||
HandleGlobalRequest(ch <-chan *ssh.Request)
|
||||
HandleTCPIPForward(req *ssh.Request)
|
||||
@@ -26,104 +21,123 @@ type Session interface {
|
||||
}
|
||||
|
||||
type SSHSession struct {
|
||||
Lifecycle lifecycle.SessionLifecycle
|
||||
Interaction interaction.Controller
|
||||
Forwarder forwarder.ForwardingController
|
||||
SlugManager slug.Manager
|
||||
initialReq <-chan *ssh.Request
|
||||
sshReqChannel <-chan ssh.NewChannel
|
||||
lifecycle lifecycle.SessionLifecycle
|
||||
interaction interaction.Controller
|
||||
forwarder forwarder.ForwardingController
|
||||
slugManager slug.Manager
|
||||
registry Registry
|
||||
userID string
|
||||
}
|
||||
|
||||
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, userID string) *SSHSession {
|
||||
slugManager := slug.NewManager()
|
||||
forwarderManager := &forwarder.Forwarder{
|
||||
Listener: nil,
|
||||
TunnelType: "",
|
||||
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,
|
||||
}
|
||||
forwarderManager := forwarder.NewForwarder(slugManager)
|
||||
interactionManager := interaction.NewInteraction(slugManager, forwarderManager)
|
||||
lifecycleManager := lifecycle.NewLifecycle(conn, forwarderManager, slugManager)
|
||||
|
||||
interactionManager.SetLifecycle(lifecycleManager)
|
||||
interactionManager.SetSlugModificator(updateClientSlug)
|
||||
interactionManager.SetSlugModificator(sessionRegistry.Update)
|
||||
forwarderManager.SetLifecycle(lifecycleManager)
|
||||
lifecycleManager.SetUnregisterClient(unregisterClient)
|
||||
lifecycleManager.SetUnregisterClient(sessionRegistry.Remove)
|
||||
|
||||
session := &SSHSession{
|
||||
Lifecycle: lifecycleManager,
|
||||
Interaction: interactionManager,
|
||||
Forwarder: forwarderManager,
|
||||
SlugManager: slugManager,
|
||||
return &SSHSession{
|
||||
initialReq: forwardingReq,
|
||||
sshReqChannel: sshChan,
|
||||
lifecycle: lifecycleManager,
|
||||
interaction: interactionManager,
|
||||
forwarder: forwarderManager,
|
||||
slugManager: slugManager,
|
||||
registry: sessionRegistry,
|
||||
userID: userID,
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
go session.Lifecycle.WaitForRunningStatus()
|
||||
type Detail struct {
|
||||
ForwardingType string `json:"forwarding_type,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Active bool `json:"active,omitempty"`
|
||||
StartedAt time.Time `json:"started_at,omitempty"`
|
||||
}
|
||||
|
||||
for channel := range sshChan {
|
||||
ch, reqs, _ := channel.Accept()
|
||||
if session.Lifecycle.GetChannel() == nil {
|
||||
session.Lifecycle.SetChannel(ch)
|
||||
session.Interaction.SetChannel(ch)
|
||||
session.Lifecycle.SetStatus(types.SETUP)
|
||||
go session.HandleGlobalRequest(forwardingReq)
|
||||
func (s *SSHSession) Detail() Detail {
|
||||
return Detail{
|
||||
ForwardingType: string(s.forwarder.GetTunnelType()),
|
||||
Slug: s.slugManager.Get(),
|
||||
UserID: s.userID,
|
||||
Active: s.lifecycle.IsActive(),
|
||||
StartedAt: s.lifecycle.StartedAt(),
|
||||
}
|
||||
go session.HandleGlobalRequest(reqs)
|
||||
}
|
||||
err := session.Lifecycle.Close()
|
||||
|
||||
func (s *SSHSession) Start() error {
|
||||
channel := <-s.sshReqChannel
|
||||
ch, reqs, err := channel.Accept()
|
||||
if err != nil {
|
||||
log.Printf("failed to accept channel: %v", err)
|
||||
return err
|
||||
}
|
||||
go s.HandleGlobalRequest(reqs)
|
||||
|
||||
tcpipReq := s.waitForTCPIPForward()
|
||||
if tcpipReq == nil {
|
||||
_, err := ch.Write([]byte(fmt.Sprintf("Port forwarding request not received. Ensure you ran the correct command with -R flag. Example: ssh %s -p %s -R 80:localhost:3000", config.Getenv("DOMAIN", "localhost"), config.Getenv("PORT", "2200"))))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.lifecycle.Close(); err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
return
|
||||
}()
|
||||
return fmt.Errorf("no forwarding Request")
|
||||
}
|
||||
|
||||
func updateClientSlug(oldSlug, newSlug string) bool {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
s.lifecycle.SetChannel(ch)
|
||||
s.interaction.SetChannel(ch)
|
||||
|
||||
if _, exists := Clients[newSlug]; exists && newSlug != oldSlug {
|
||||
return false
|
||||
s.HandleTCPIPForward(tcpipReq)
|
||||
|
||||
if err := s.lifecycle.Close(); err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
client, ok := Clients[oldSlug]
|
||||
func (s *SSHSession) waitForTCPIPForward() *ssh.Request {
|
||||
select {
|
||||
case req, ok := <-s.initialReq:
|
||||
if !ok {
|
||||
return false
|
||||
log.Println("Forwarding request channel closed")
|
||||
return nil
|
||||
}
|
||||
|
||||
delete(Clients, oldSlug)
|
||||
client.SlugManager.Set(newSlug)
|
||||
Clients[newSlug] = client
|
||||
return true
|
||||
if req.Type == "tcpip-forward" {
|
||||
return req
|
||||
}
|
||||
|
||||
func registerClient(slug string, session *SSHSession) bool {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
if _, exists := Clients[slug]; exists {
|
||||
return false
|
||||
if err := req.Reply(false, nil); err != nil {
|
||||
log.Printf("Failed to reply to request: %v", err)
|
||||
}
|
||||
|
||||
Clients[slug] = session
|
||||
return true
|
||||
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
|
||||
}
|
||||
|
||||
func unregisterClient(slug string) {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
delete(Clients, slug)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package slug
|
||||
|
||||
import "sync"
|
||||
|
||||
type Manager interface {
|
||||
Get() string
|
||||
Set(slug string)
|
||||
@@ -9,24 +7,18 @@ type Manager interface {
|
||||
|
||||
type manager struct {
|
||||
slug string
|
||||
slugMu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewManager() Manager {
|
||||
return &manager{
|
||||
slug: "",
|
||||
slugMu: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *manager) Get() string {
|
||||
s.slugMu.RLock()
|
||||
defer s.slugMu.RUnlock()
|
||||
return s.slug
|
||||
}
|
||||
|
||||
func (s *manager) Set(slug string) {
|
||||
s.slugMu.Lock()
|
||||
s.slug = slug
|
||||
s.slugMu.Unlock()
|
||||
}
|
||||
|
||||
@@ -14,3 +14,8 @@ const (
|
||||
HTTP TunnelType = "HTTP"
|
||||
TCP TunnelType = "TCP"
|
||||
)
|
||||
|
||||
var BadGatewayResponse = []byte("HTTP/1.1 502 Bad Gateway\r\n" +
|
||||
"Content-Length: 11\r\n" +
|
||||
"Content-Type: text/plain\r\n\r\n" +
|
||||
"Bad Gateway")
|
||||
|
||||
@@ -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