Compare commits
96 Commits
8c8fdf251d
...
v1.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 6213ff8a30 | |||
| 4ffaec9d9a | |||
| 6de0a618ee | |||
| 8cc70fa45e | |||
| 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 | |||
| fd513d7bc9 | |||
| 73e7df6a3b | |||
| d2c5b2a4db | |||
| 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
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
build-args: |
|
||||
VERSION=${{ steps.vars.outputs.VERSION }}
|
||||
BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }}
|
||||
COMMIT=${{ steps.vars.outputs.COMMIT }}
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
|
||||
build-and-push-tags:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.fossy.my.id
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract version and determine release type
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
echo "COMMIT=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
|
||||
if echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
MINOR=$(echo "$VERSION" | cut -d. -f2)
|
||||
|
||||
echo "MAJOR=$MAJOR" >> $GITHUB_OUTPUT
|
||||
echo "MINOR=$MINOR" >> $GITHUB_OUTPUT
|
||||
|
||||
if echo "$VERSION" | grep -q '-'; then
|
||||
echo "IS_PRERELEASE=true" >> $GITHUB_OUTPUT
|
||||
echo "ADDITIONAL_TAG=staging" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "IS_PRERELEASE=false" >> $GITHUB_OUTPUT
|
||||
echo "ADDITIONAL_TAG=latest" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "Invalid version format: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and push Docker image for release
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.VERSION }}
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.MAJOR }}.${{ steps.version.outputs.MINOR }}
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.MAJOR }}
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:${{ steps.version.outputs.ADDITIONAL_TAG }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.VERSION }}
|
||||
BUILD_DATE=${{ steps.version.outputs.BUILD_DATE }}
|
||||
COMMIT=${{ steps.version.outputs.COMMIT }}
|
||||
if: steps.version.outputs.IS_PRERELEASE == 'false'
|
||||
|
||||
- name: Build and push Docker image for pre-release
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.VERSION }}
|
||||
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:${{ steps.version.outputs.ADDITIONAL_TAG }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.VERSION }}
|
||||
BUILD_DATE=${{ steps.version.outputs.BUILD_DATE }}
|
||||
COMMIT=${{ steps.version.outputs.COMMIT }}
|
||||
if: steps.version.outputs.IS_PRERELEASE == 'true'
|
||||
|
||||
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"]
|
||||
|
||||
@@ -1,157 +1,157 @@
|
||||
# Attribution-NonCommercial-NoDerivatives 4.0 International
|
||||
|
||||
> *Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.*
|
||||
>
|
||||
> ### Using Creative Commons Public Licenses
|
||||
>
|
||||
> Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
|
||||
>
|
||||
> * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors).
|
||||
>
|
||||
> * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees).
|
||||
|
||||
## Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
|
||||
|
||||
### Section 1 – Definitions.
|
||||
|
||||
a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
|
||||
|
||||
b. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
|
||||
|
||||
e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
|
||||
|
||||
f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
|
||||
|
||||
h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
|
||||
|
||||
i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License.
|
||||
|
||||
i. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.
|
||||
|
||||
j. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
|
||||
|
||||
k. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
|
||||
|
||||
l. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
|
||||
|
||||
### Section 2 – Scope.
|
||||
|
||||
a. ___License grant.___
|
||||
|
||||
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and
|
||||
|
||||
B. produce and reproduce, but not Share, Adapted Material for NonCommercial purposes only.
|
||||
|
||||
2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
|
||||
|
||||
3. __Term.__ The term of this Public License is specified in Section 6(a).
|
||||
|
||||
4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
|
||||
|
||||
5. __Downstream recipients.__
|
||||
|
||||
A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
|
||||
|
||||
B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
|
||||
|
||||
6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. ___Other rights.___
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes.
|
||||
|
||||
### Section 3 – License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
|
||||
|
||||
a. ___Attribution.___
|
||||
|
||||
1. If You Share the Licensed Material, You must:
|
||||
|
||||
A. retain the following if it is supplied by the Licensor with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
|
||||
|
||||
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
|
||||
|
||||
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
|
||||
|
||||
For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
|
||||
|
||||
### Section 4 – Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only and provided You do not Share Adapted Material;
|
||||
|
||||
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
|
||||
|
||||
### Section 5 – Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__
|
||||
|
||||
b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
|
||||
|
||||
### Section 6 – Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
|
||||
|
||||
### Section 7 – Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
|
||||
|
||||
### Section 8 – Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
|
||||
|
||||
> Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
|
||||
>
|
||||
# Attribution-NonCommercial-NoDerivatives 4.0 International
|
||||
|
||||
> *Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.*
|
||||
>
|
||||
> ### Using Creative Commons Public Licenses
|
||||
>
|
||||
> Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
|
||||
>
|
||||
> * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors).
|
||||
>
|
||||
> * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees).
|
||||
|
||||
## Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
|
||||
|
||||
### Section 1 – Definitions.
|
||||
|
||||
a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
|
||||
|
||||
b. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
|
||||
|
||||
e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
|
||||
|
||||
f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
|
||||
|
||||
h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
|
||||
|
||||
i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License.
|
||||
|
||||
i. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.
|
||||
|
||||
j. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
|
||||
|
||||
k. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
|
||||
|
||||
l. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
|
||||
|
||||
### Section 2 – Scope.
|
||||
|
||||
a. ___License grant.___
|
||||
|
||||
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and
|
||||
|
||||
B. produce and reproduce, but not Share, Adapted Material for NonCommercial purposes only.
|
||||
|
||||
2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
|
||||
|
||||
3. __Term.__ The term of this Public License is specified in Section 6(a).
|
||||
|
||||
4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
|
||||
|
||||
5. __Downstream recipients.__
|
||||
|
||||
A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
|
||||
|
||||
B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
|
||||
|
||||
6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. ___Other rights.___
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes.
|
||||
|
||||
### Section 3 – License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
|
||||
|
||||
a. ___Attribution.___
|
||||
|
||||
1. If You Share the Licensed Material, You must:
|
||||
|
||||
A. retain the following if it is supplied by the Licensor with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
|
||||
|
||||
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
|
||||
|
||||
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
|
||||
|
||||
For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
|
||||
|
||||
### Section 4 – Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only and provided You do not Share Adapted Material;
|
||||
|
||||
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
|
||||
|
||||
### Section 5 – Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__
|
||||
|
||||
b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
|
||||
|
||||
### Section 6 – Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
|
||||
|
||||
### Section 7 – Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
|
||||
|
||||
### Section 8 – Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
|
||||
|
||||
> Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
|
||||
>
|
||||
> Creative Commons may be contacted at [creativecommons.org](http://creativecommons.org).
|
||||
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.5.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
|
||||
)
|
||||
155
go.sum
155
go.sum
@@ -1,13 +1,146 @@
|
||||
git.fossy.my.id/bagas/tunnel-please-grpc v1.3.0 h1:RhcBKUG41/om4jgN+iF/vlY/RojTeX1QhBa4p4428ec=
|
||||
git.fossy.my.id/bagas/tunnel-please-grpc v1.3.0/go.mod h1:fG+VkArdkceGB0bNA7IFQus9GetLAwdF5Oi4jdMlXtY=
|
||||
git.fossy.my.id/bagas/tunnel-please-grpc v1.4.0 h1:tpJSKjaSmV+vxxbVx6qnStjxFVXjj2M0rygWXxLb99o=
|
||||
git.fossy.my.id/bagas/tunnel-please-grpc v1.4.0/go.mod h1:fG+VkArdkceGB0bNA7IFQus9GetLAwdF5Oi4jdMlXtY=
|
||||
git.fossy.my.id/bagas/tunnel-please-grpc v1.5.0 h1:3xszIhck4wo9CoeRq9vnkar4PhY7kz9QrR30qj2XszA=
|
||||
git.fossy.my.id/bagas/tunnel-please-grpc v1.5.0/go.mod h1:Weh6ZujgWmT8XxD3Qba7sJ6r5eyUMB9XSWynqdyOoLo=
|
||||
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
|
||||
}
|
||||
474
internal/grpc/client/client.go
Normal file
474
internal/grpc/client/client.go
Normal file
@@ -0,0 +1,474 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
"tunnel_pls/internal/config"
|
||||
"tunnel_pls/types"
|
||||
|
||||
"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
|
||||
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)
|
||||
}
|
||||
|
||||
eventService := proto.NewEventServiceClient(conn)
|
||||
authorizeConnectionService := proto.NewUserServiceClient(conn)
|
||||
|
||||
return &Client{
|
||||
conn: conn,
|
||||
config: config,
|
||||
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 {
|
||||
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:
|
||||
user := recv.GetSlugEvent().GetUser()
|
||||
oldSlug := recv.GetSlugEvent().GetOld()
|
||||
newSlug := recv.GetSlugEvent().GetNew()
|
||||
var userSession *session.SSHSession
|
||||
userSession, err = c.sessionRegistry.Get(types.SessionKey{
|
||||
Id: oldSlug,
|
||||
Type: types.HTTP,
|
||||
})
|
||||
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(user, types.SessionKey{
|
||||
Id: oldSlug,
|
||||
Type: types.HTTP,
|
||||
}, types.SessionKey{
|
||||
Id: newSlug,
|
||||
Type: types.HTTP,
|
||||
})
|
||||
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
|
||||
}
|
||||
userSession.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
|
||||
}
|
||||
case proto.EventType_TERMINATE_SESSION:
|
||||
user := recv.GetTerminateSessionEvent().GetUser()
|
||||
tunnelTypeRaw := recv.GetTerminateSessionEvent().GetTunnelType()
|
||||
slug := recv.GetTerminateSessionEvent().GetSlug()
|
||||
|
||||
var userSession *session.SSHSession
|
||||
var tunnelType types.TunnelType
|
||||
if tunnelTypeRaw == proto.TunnelType_HTTP {
|
||||
tunnelType = types.HTTP
|
||||
} else if tunnelTypeRaw == proto.TunnelType_TCP {
|
||||
tunnelType = types.TCP
|
||||
} else {
|
||||
err = subscribe.Send(&proto.Node{
|
||||
Type: proto.EventType_TERMINATE_SESSION,
|
||||
Payload: &proto.Node_TerminateSessionEventResponse{
|
||||
TerminateSessionEventResponse: &proto.TerminateSessionEventResponse{
|
||||
Success: false,
|
||||
Message: "unknown tunnel type recived",
|
||||
},
|
||||
},
|
||||
})
|
||||
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
|
||||
}
|
||||
userSession, err = c.sessionRegistry.GetWithUser(user, types.SessionKey{
|
||||
Id: slug,
|
||||
Type: tunnelType,
|
||||
})
|
||||
if err != nil {
|
||||
err = subscribe.Send(&proto.Node{
|
||||
Type: proto.EventType_TERMINATE_SESSION,
|
||||
Payload: &proto.Node_TerminateSessionEventResponse{
|
||||
TerminateSessionEventResponse: &proto.TerminateSessionEventResponse{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
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
|
||||
}
|
||||
err = userSession.GetLifecycle().Close()
|
||||
if err != nil {
|
||||
err = subscribe.Send(&proto.Node{
|
||||
Type: proto.EventType_TERMINATE_SESSION,
|
||||
Payload: &proto.Node_TerminateSessionEventResponse{
|
||||
TerminateSessionEventResponse: &proto.TerminateSessionEventResponse{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
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
|
||||
}
|
||||
err = subscribe.Send(&proto.Node{
|
||||
Type: proto.EventType_TERMINATE_SESSION,
|
||||
Payload: &proto.Node_TerminateSessionEventResponse{
|
||||
TerminateSessionEventResponse: &proto.TerminateSessionEventResponse{
|
||||
Success: true,
|
||||
Message: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
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 {
|
||||
start, err := strconv.ParseUint(splitRange[0], 10, 16)
|
||||
if err != nil {
|
||||
start = 30000
|
||||
}
|
||||
end, err := strconv.ParseUint(splitRange[1], 10, 16)
|
||||
if err != nil {
|
||||
end = 31000
|
||||
}
|
||||
Manager.AddPortRange(uint16(start), uint16(end))
|
||||
return
|
||||
}
|
||||
|
||||
start, err := strconv.ParseUint(splitRange[0], 10, 16)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
end, err := strconv.ParseUint(splitRange[1], 10, 16)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = Default.AddPortRange(uint16(start), uint16(end))
|
||||
}
|
||||
|
||||
func (pm *PortManager) AddPortRange(startPort, endPort uint16) error {
|
||||
func (pm *manager) AddPortRange(startPort, endPort uint16) error {
|
||||
pm.mu.Lock()
|
||||
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()
|
||||
}
|
||||
127
main.go
127
main.go
@@ -1,24 +1,63 @@
|
||||
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() {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
sshConfig := &ssh.ServerConfig{
|
||||
NoClientAuth: true,
|
||||
ServerVersion: "SSH-2.0-TunnlPls-1.0",
|
||||
if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-v") {
|
||||
fmt.Println(version.GetVersion())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
privateBytes, err := os.ReadFile(utils.Getenv("ssh_private_key"))
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -29,6 +68,74 @@ func main() {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
193
server/header.go
193
server/header.go
@@ -4,7 +4,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HeaderManager interface {
|
||||
@@ -14,63 +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(br *bufio.Reader) (*RequestHeaderFactory, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func parseHeadersFromBytes(headerData []byte) (RequestHeaderManager, error) {
|
||||
header := &requestHeaderFactory{
|
||||
headers: make(map[string]string, 16),
|
||||
}
|
||||
|
||||
startLine, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
lineEnd := bytes.IndexByte(headerData, '\n')
|
||||
if lineEnd == -1 {
|
||||
return nil, fmt.Errorf("invalid request: no newline found")
|
||||
}
|
||||
startLine = strings.TrimRight(startLine, "\r\n")
|
||||
header.startLine = []byte(startLine)
|
||||
|
||||
parts := strings.Split(startLine, " ")
|
||||
startLine := bytes.TrimRight(headerData[:lineEnd], "\r\n")
|
||||
header.startLine = make([]byte, len(startLine))
|
||||
copy(header.startLine, startLine)
|
||||
|
||||
parts := bytes.Split(startLine, []byte{' '})
|
||||
if len(parts) < 3 {
|
||||
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),
|
||||
}
|
||||
@@ -96,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)
|
||||
@@ -125,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 ""
|
||||
@@ -133,15 +238,27 @@ 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)
|
||||
|
||||
224
server/http.go
224
server/http.go
@@ -10,42 +10,63 @@ import (
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"tunnel_pls/internal/config"
|
||||
"tunnel_pls/session"
|
||||
"tunnel_pls/types"
|
||||
"tunnel_pls/utils"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Interaction interface {
|
||||
SendMessage(message string)
|
||||
}
|
||||
type CustomWriter struct {
|
||||
RemoteAddr net.Addr
|
||||
writer io.Writer
|
||||
reader io.Reader
|
||||
headerBuf []byte
|
||||
buf []byte
|
||||
respHeader *ResponseHeaderFactory
|
||||
reqHeader *RequestHeaderFactory
|
||||
interaction Interaction
|
||||
respMW []ResponseMiddleware
|
||||
reqStartMW []RequestMiddleware
|
||||
reqEndMW []RequestMiddleware
|
||||
overflow []byte
|
||||
type HTTPWriter interface {
|
||||
io.Reader
|
||||
io.Writer
|
||||
GetRemoteAddr() net.Addr
|
||||
GetWriter() io.Writer
|
||||
AddResponseMiddleware(mw ResponseMiddleware)
|
||||
AddRequestStartMiddleware(mw RequestMiddleware)
|
||||
SetRequestHeader(header RequestHeaderManager)
|
||||
GetRequestStartMiddleware() []RequestMiddleware
|
||||
}
|
||||
|
||||
func (cw *CustomWriter) SetInteraction(interaction Interaction) {
|
||||
cw.interaction = interaction
|
||||
type customWriter struct {
|
||||
remoteAddr net.Addr
|
||||
writer io.Writer
|
||||
reader io.Reader
|
||||
headerBuf []byte
|
||||
buf []byte
|
||||
respHeader ResponseHeaderManager
|
||||
reqHeader RequestHeaderManager
|
||||
respMW []ResponseMiddleware
|
||||
reqStartMW []RequestMiddleware
|
||||
reqEndMW []RequestMiddleware
|
||||
}
|
||||
|
||||
func (cw *CustomWriter) Read(p []byte) (int, error) {
|
||||
if len(cw.overflow) > 0 {
|
||||
n := copy(p, cw.overflow)
|
||||
cw.overflow = cw.overflow[n:]
|
||||
if len(cw.overflow) == 0 {
|
||||
cw.overflow = nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
func (cw *customWriter) GetRemoteAddr() net.Addr {
|
||||
return cw.remoteAddr
|
||||
}
|
||||
|
||||
func (cw *customWriter) GetWriter() io.Writer {
|
||||
return cw.writer
|
||||
}
|
||||
|
||||
func (cw *customWriter) AddResponseMiddleware(mw ResponseMiddleware) {
|
||||
cw.respMW = append(cw.respMW, mw)
|
||||
}
|
||||
|
||||
func (cw *customWriter) AddRequestStartMiddleware(mw RequestMiddleware) {
|
||||
cw.reqStartMW = append(cw.reqStartMW, mw)
|
||||
}
|
||||
|
||||
func (cw *customWriter) SetRequestHeader(header RequestHeaderManager) {
|
||||
cw.reqHeader = header
|
||||
}
|
||||
|
||||
func (cw *customWriter) GetRequestStartMiddleware() []RequestMiddleware {
|
||||
return cw.reqStartMW
|
||||
}
|
||||
|
||||
func (cw *customWriter) Read(p []byte) (int, error) {
|
||||
tmp := make([]byte, len(p))
|
||||
read, err := cw.reader.Read(tmp)
|
||||
if read == 0 && err != nil {
|
||||
@@ -79,8 +100,7 @@ func (cw *CustomWriter) Read(p []byte) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
headerReader := bufio.NewReader(bytes.NewReader(header))
|
||||
reqhf, err := NewRequestHeaderFactory(headerReader)
|
||||
reqhf, err := NewRequestHeaderFactory(header)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -99,26 +119,19 @@ func (cw *CustomWriter) Read(p []byte) (int, error) {
|
||||
|
||||
n := copy(p, combined)
|
||||
|
||||
if n > len(p) {
|
||||
cw.overflow = make([]byte, len(combined)-n)
|
||||
copy(cw.overflow, combined[n:])
|
||||
log.Printf("output buffer too small (%d vs %d)", len(p), n)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func NewCustomWriter(writer io.Writer, reader io.Reader, remoteAddr net.Addr) *CustomWriter {
|
||||
return &CustomWriter{
|
||||
RemoteAddr: remoteAddr,
|
||||
writer: writer,
|
||||
reader: reader,
|
||||
buf: make([]byte, 0, 4096),
|
||||
interaction: nil,
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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} .+`)
|
||||
|
||||
@@ -142,9 +155,9 @@ func isHTTPHeader(buf []byte) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (cw *CustomWriter) Write(p []byte) (int, error) {
|
||||
if len(p) == len(types.BadGatewayResponse) && bytes.Equal(p, types.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 {
|
||||
@@ -166,9 +179,12 @@ func (cw *CustomWriter) Write(p []byte) (int, error) {
|
||||
body := cw.buf[idx+len(DELIMITER):]
|
||||
|
||||
if !isHTTPHeader(header) {
|
||||
n, err := cw.writer.Write(cw.buf)
|
||||
_, err := cw.writer.Write(cw.buf)
|
||||
cw.buf = nil
|
||||
return n, err
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
resphf := NewResponseHeaderFactory(header)
|
||||
@@ -196,18 +212,29 @@ func (cw *CustomWriter) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (cw *CustomWriter) AddInteraction(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() {
|
||||
@@ -222,13 +249,13 @@ 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 && !errors.Is(err, net.ErrClosed) {
|
||||
@@ -259,7 +286,7 @@ func Handler(conn net.Conn) {
|
||||
|
||||
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")) +
|
||||
fmt.Sprintf("Location: https://%s.%s/\r\n", slug, config.Getenv("DOMAIN", "localhost")) +
|
||||
"Content-Length: 0\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
"\r\n"))
|
||||
@@ -287,8 +314,11 @@ func Handler(conn net.Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
sshSession, ok := session.Clients[slug]
|
||||
if !ok {
|
||||
sshSession, err := hs.sessionRegistry.Get(types.SessionKey{
|
||||
Id: slug,
|
||||
Type: types.HTTP,
|
||||
})
|
||||
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" +
|
||||
@@ -301,53 +331,65 @@ func Handler(conn net.Conn) {
|
||||
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)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
//TODO: Implement wrapper func buat add/remove middleware
|
||||
|
||||
go ssh.DiscardRequests(reqs)
|
||||
|
||||
fingerprintMiddleware := NewTunnelFingerprint()
|
||||
loggerMiddleware := NewRequestLogger(cw.interaction, cw.RemoteAddr)
|
||||
forwardedForMiddleware := NewForwardedFor(cw.RemoteAddr)
|
||||
forwardedForMiddleware := NewForwardedFor(cw.GetRemoteAddr())
|
||||
|
||||
cw.respMW = append(cw.respMW, fingerprintMiddleware)
|
||||
cw.reqStartMW = append(cw.reqStartMW, loggerMiddleware)
|
||||
cw.reqStartMW = append(cw.reqStartMW, forwardedForMiddleware)
|
||||
//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
|
||||
}
|
||||
|
||||
sshSession.GetForwarder().HandleConnection(cw, channel, cw.GetRemoteAddr())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,18 +8,20 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"tunnel_pls/session"
|
||||
"tunnel_pls/utils"
|
||||
"tunnel_pls/internal/config"
|
||||
"tunnel_pls/types"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -36,13 +38,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 {
|
||||
@@ -88,8 +90,11 @@ func HandlerTLS(conn net.Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
sshSession, ok := session.Clients[slug]
|
||||
if !ok {
|
||||
sshSession, err := hs.sessionRegistry.Get(types.SessionKey{
|
||||
Id: slug,
|
||||
Type: types.HTTP,
|
||||
})
|
||||
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" +
|
||||
@@ -102,7 +107,6 @@ func HandlerTLS(conn net.Conn) {
|
||||
return
|
||||
}
|
||||
cw := NewCustomWriter(conn, dstReader, conn.RemoteAddr())
|
||||
cw.SetInteraction(sshSession.Interaction)
|
||||
forwardRequest(cw, reqhf, sshSession)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
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,28 +18,11 @@ func NewTunnelFingerprint() *TunnelFingerprint {
|
||||
return &TunnelFingerprint{}
|
||||
}
|
||||
|
||||
func (h *TunnelFingerprint) HandleResponse(header *ResponseHeaderFactory, body []byte) error {
|
||||
func (h *TunnelFingerprint) HandleResponse(header ResponseHeaderManager, body []byte) error {
|
||||
header.Set("Server", "Tunnel Please")
|
||||
return nil
|
||||
}
|
||||
|
||||
type RequestLogger struct {
|
||||
interaction Interaction
|
||||
remoteAddr net.Addr
|
||||
}
|
||||
|
||||
func NewRequestLogger(interaction Interaction, remoteAddr net.Addr) *RequestLogger {
|
||||
return &RequestLogger{
|
||||
interaction: interaction,
|
||||
remoteAddr: remoteAddr,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *RequestLogger) HandleRequest(header *RequestHeaderFactory) error {
|
||||
rl.interaction.SendMessage(fmt.Sprintf("\033[32m%s %s -> %s %s \033[0m\r\n", time.Now().UTC().Format(time.RFC3339), rl.remoteAddr.String(), header.Method, header.Path))
|
||||
return nil
|
||||
}
|
||||
|
||||
type ForwardedFor struct {
|
||||
addr net.Addr
|
||||
}
|
||||
@@ -50,7 +31,7 @@ func NewForwardedFor(addr net.Addr) *ForwardedFor {
|
||||
return &ForwardedFor{addr: addr}
|
||||
}
|
||||
|
||||
func (ff *ForwardedFor) HandleRequest(header *RequestHeaderFactory) error {
|
||||
func (ff *ForwardedFor) HandleRequest(header RequestHeaderManager) error {
|
||||
host, _, err := net.SplitHostPort(ff.addr.String())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -58,119 +39,3 @@ func (ff *ForwardedFor) HandleRequest(header *RequestHeaderFactory) error {
|
||||
header.Set("X-Forwarded-For", host)
|
||||
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
|
||||
}()
|
||||
|
||||
HttpServer := NewHTTPServer(sessionRegistry)
|
||||
err = HttpServer.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to start http server: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
err = NewHTTPServer()
|
||||
|
||||
if config.Getenv("TLS_ENABLED", "false") == "true" {
|
||||
err = HttpServer.ListenAndServeTLS()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to start http server: %v", err)
|
||||
log.Fatalf("failed to start https server: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}()
|
||||
return &Server{
|
||||
Conn: &listener,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
return &Server{
|
||||
conn: &listener,
|
||||
config: sshConfig,
|
||||
sessionRegistry: sessionRegistry,
|
||||
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,38 @@ 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 {
|
||||
@@ -42,7 +68,7 @@ type ForwardingController interface {
|
||||
}
|
||||
|
||||
func (f *Forwarder) SetLifecycle(lifecycle Lifecycle) {
|
||||
f.Lifecycle = lifecycle
|
||||
f.lifecycle = lifecycle
|
||||
}
|
||||
|
||||
func (f *Forwarder) AcceptTCPConnections() {
|
||||
@@ -55,28 +81,57 @@ 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) {
|
||||
defer func() {
|
||||
_, err := io.Copy(io.Discard, src)
|
||||
if err != nil {
|
||||
log.Printf("Failed to discard connection: %v", err)
|
||||
@@ -84,48 +139,63 @@ func (f *Forwarder) HandleConnection(dst io.ReadWriter, src ssh.Channel, remoteA
|
||||
|
||||
err = src.Close()
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Printf("Error closing connection: %v", err)
|
||||
log.Printf("Error closing source channel: %v", err)
|
||||
}
|
||||
}(src)
|
||||
log.Printf("Handling new forwarded connection from %s", remoteAddr)
|
||||
|
||||
go func() {
|
||||
_, 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)
|
||||
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)
|
||||
}
|
||||
return
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
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) {
|
||||
@@ -137,8 +207,8 @@ func (f *Forwarder) WriteBadGatewayResponse(dst io.Writer) {
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
case "shell", "pty-req", "window-change":
|
||||
case "shell", "pty-req":
|
||||
err := req.Reply(true, nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
return
|
||||
}
|
||||
case "window-change":
|
||||
p := req.Payload
|
||||
if len(p) < 16 {
|
||||
log.Println("invalid window-change payload")
|
||||
err := req.Reply(false, nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
cols := binary.BigEndian.Uint32(p[0:4])
|
||||
rows := binary.BigEndian.Uint32(p[4:8])
|
||||
|
||||
s.interaction.SetWH(int(cols), int(rows))
|
||||
|
||||
err := req.Reply(true, nil)
|
||||
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,15 +106,14 @@ 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)
|
||||
}
|
||||
@@ -108,56 +123,50 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
|
||||
if portToBind == 80 || portToBind == 443 {
|
||||
s.HandleHTTPForward(req, portToBind)
|
||||
return
|
||||
} else {
|
||||
if portToBind == 0 {
|
||||
unassign, success := portUtil.Manager.GetUnassignedPort()
|
||||
portToBind = unassign
|
||||
if !success {
|
||||
s.Interaction.SendMessage("No available port\r\n")
|
||||
err := req.Reply(false, nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
return
|
||||
}
|
||||
err = s.Lifecycle.Close()
|
||||
if err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if isUse, isExist := portUtil.Manager.GetPortStatus(portToBind); isExist && isUse {
|
||||
s.Interaction.SendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port. (03)\r\n", portToBind))
|
||||
}
|
||||
if portToBind == 0 {
|
||||
unassign, success := portUtil.Default.GetUnassignedPort()
|
||||
portToBind = unassign
|
||||
if !success {
|
||||
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
|
||||
}
|
||||
err := portUtil.Manager.SetPortStatus(portToBind, true)
|
||||
} else if isUse, isExist := portUtil.Default.GetPortStatus(portToBind); isExist && isUse {
|
||||
log.Printf("Port %d is already in use or restricted", portToBind)
|
||||
err := req.Reply(false, nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to set port status:", err)
|
||||
log.Println("Failed to reply to request:", err)
|
||||
return
|
||||
}
|
||||
err = s.lifecycle.Close()
|
||||
if err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
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) {
|
||||
slug := generateUniqueSlug()
|
||||
if slug == "" {
|
||||
err := req.Reply(false, nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
slug := random.GenerateRandomString(20)
|
||||
key := types.SessionKey{Id: slug, Type: types.HTTP}
|
||||
|
||||
if !registerClient(slug, s) {
|
||||
if !s.registry.Register(key, s) {
|
||||
log.Printf("Failed to register client with slug: %s", slug)
|
||||
err := req.Reply(false, nil)
|
||||
if err != nil {
|
||||
@@ -170,7 +179,7 @@ func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
|
||||
err := binary.Write(buf, binary.BigEndian, uint32(portToBind))
|
||||
if err != nil {
|
||||
log.Println("Failed to write port to buffer:", err)
|
||||
unregisterClient(slug)
|
||||
s.registry.Remove(key)
|
||||
err = req.Reply(false, nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
@@ -179,16 +188,10 @@ func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
|
||||
}
|
||||
log.Printf("HTTP forwarding approved on port: %d", portToBind)
|
||||
|
||||
domain := utils.Getenv("domain")
|
||||
protocol := "http"
|
||||
if utils.Getenv("tls_enabled") == "true" {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
err = req.Reply(true, buf.Bytes())
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
unregisterClient(slug)
|
||||
s.registry.Remove(key)
|
||||
err = req.Reply(false, nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
@@ -196,37 +199,59 @@ func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
|
||||
return
|
||||
}
|
||||
|
||||
s.Forwarder.SetType(types.HTTP)
|
||||
s.Forwarder.SetForwardedPort(portToBind)
|
||||
s.SlugManager.Set(slug)
|
||||
s.Interaction.SendMessage("\033[H\033[2J")
|
||||
s.Interaction.ShowWelcomeMessage()
|
||||
s.Interaction.SendMessage(fmt.Sprintf("Forwarding your traffic to %s://%s.%s\r\n", protocol, slug, domain))
|
||||
s.Lifecycle.SetStatus(types.RUNNING)
|
||||
s.Interaction.HandleUserInput()
|
||||
s.forwarder.SetType(types.HTTP)
|
||||
s.forwarder.SetForwardedPort(portToBind)
|
||||
s.slugManager.Set(slug)
|
||||
s.lifecycle.SetStatus(types.RUNNING)
|
||||
s.interaction.Start()
|
||||
}
|
||||
|
||||
func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind uint16) {
|
||||
log.Printf("Requested forwarding on %s:%d", addr, portToBind)
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", portToBind))
|
||||
if err != nil {
|
||||
s.Interaction.SendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port.\r\n", portToBind))
|
||||
log.Printf("Port %d is already in use or restricted", portToBind)
|
||||
if setErr := portUtil.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
|
||||
}
|
||||
|
||||
key := types.SessionKey{Id: fmt.Sprintf("%d", portToBind), Type: types.TCP}
|
||||
|
||||
if !s.registry.Register(key, s) {
|
||||
log.Printf("Failed to register TCP client with id: %s", key.Id)
|
||||
if setErr := portUtil.Default.SetPortStatus(portToBind, false); setErr != nil {
|
||||
log.Printf("Failed to reset port status: %v", setErr)
|
||||
}
|
||||
if closeErr := listener.Close(); closeErr != nil {
|
||||
log.Printf("Failed to close listener: %s", closeErr)
|
||||
}
|
||||
err = req.Reply(false, nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
}
|
||||
_ = s.lifecycle.Close()
|
||||
return
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err = binary.Write(buf, binary.BigEndian, uint32(portToBind))
|
||||
if err != nil {
|
||||
log.Println("Failed to write port to buffer:", err)
|
||||
s.registry.Remove(key)
|
||||
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)
|
||||
@@ -239,6 +264,10 @@ func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind
|
||||
err = req.Reply(true, buf.Bytes())
|
||||
if err != nil {
|
||||
log.Println("Failed to reply to request:", err)
|
||||
s.registry.Remove(key)
|
||||
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)
|
||||
@@ -247,34 +276,13 @@ func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind
|
||||
return
|
||||
}
|
||||
|
||||
s.Forwarder.SetType(types.TCP)
|
||||
s.Forwarder.SetListener(listener)
|
||||
s.Forwarder.SetForwardedPort(portToBind)
|
||||
s.Interaction.SendMessage("\033[H\033[2J")
|
||||
s.Interaction.ShowWelcomeMessage()
|
||||
s.Interaction.SendMessage(fmt.Sprintf("Forwarding your traffic to tcp://%s:%d \r\n", utils.Getenv("domain"), s.Forwarder.GetForwardedPort()))
|
||||
s.Lifecycle.SetStatus(types.RUNNING)
|
||||
go s.Forwarder.AcceptTCPConnections()
|
||||
s.Interaction.HandleUserInput()
|
||||
}
|
||||
|
||||
func generateUniqueSlug() string {
|
||||
maxAttempts := 5
|
||||
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
slug := utils.GenerateRandomString(20)
|
||||
|
||||
clientsMutex.RLock()
|
||||
_, exists := Clients[slug]
|
||||
clientsMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return slug
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Failed to generate unique slug after multiple attempts")
|
||||
return ""
|
||||
s.forwarder.SetType(types.TCP)
|
||||
s.forwarder.SetListener(listener)
|
||||
s.forwarder.SetForwardedPort(portToBind)
|
||||
s.slugManager.Set(key.Id)
|
||||
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,109 +13,105 @@ import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Interaction interface {
|
||||
SendMessage(string)
|
||||
}
|
||||
|
||||
type Forwarder interface {
|
||||
Close() error
|
||||
GetTunnelType() types.TunnelType
|
||||
GetForwardedPort() uint16
|
||||
}
|
||||
|
||||
type Lifecycle struct {
|
||||
Status types.Status
|
||||
Conn ssh.Conn
|
||||
Channel ssh.Channel
|
||||
|
||||
Interaction Interaction
|
||||
Forwarder Forwarder
|
||||
SlugManager slug.Manager
|
||||
unregisterClient func(slug string)
|
||||
type SessionRegistry interface {
|
||||
Remove(key types.SessionKey)
|
||||
}
|
||||
|
||||
func (l *Lifecycle) SetUnregisterClient(unregisterClient func(slug string)) {
|
||||
l.unregisterClient = unregisterClient
|
||||
type Lifecycle struct {
|
||||
status types.Status
|
||||
conn ssh.Conn
|
||||
channel ssh.Channel
|
||||
forwarder Forwarder
|
||||
sessionRegistry SessionRegistry
|
||||
slugManager slug.Manager
|
||||
startedAt time.Time
|
||||
user string
|
||||
}
|
||||
|
||||
func NewLifecycle(conn ssh.Conn, forwarder Forwarder, slugManager slug.Manager, user string) *Lifecycle {
|
||||
return &Lifecycle{
|
||||
status: types.INITIALIZING,
|
||||
conn: conn,
|
||||
channel: nil,
|
||||
forwarder: forwarder,
|
||||
slugManager: slugManager,
|
||||
sessionRegistry: nil,
|
||||
startedAt: time.Now(),
|
||||
user: user,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lifecycle) SetSessionRegistry(registry SessionRegistry) {
|
||||
l.sessionRegistry = registry
|
||||
}
|
||||
|
||||
type SessionLifecycle interface {
|
||||
Close() error
|
||||
WaitForRunningStatus()
|
||||
SetStatus(status types.Status)
|
||||
GetConnection() ssh.Conn
|
||||
GetChannel() ssh.Channel
|
||||
GetUser() string
|
||||
SetChannel(channel ssh.Channel)
|
||||
SetUnregisterClient(unregisterClient func(slug string))
|
||||
SetSessionRegistry(registry SessionRegistry)
|
||||
IsActive() bool
|
||||
StartedAt() time.Time
|
||||
}
|
||||
|
||||
func (l *Lifecycle) GetUser() string {
|
||||
return l.user
|
||||
}
|
||||
|
||||
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()
|
||||
if clientSlug != "" {
|
||||
l.unregisterClient(clientSlug)
|
||||
clientSlug := l.slugManager.Get()
|
||||
if clientSlug != "" && l.sessionRegistry.Remove != nil {
|
||||
key := types.SessionKey{Id: clientSlug, Type: l.forwarder.GetTunnelType()}
|
||||
l.sessionRegistry.Remove(key)
|
||||
}
|
||||
|
||||
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 +119,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
|
||||
}
|
||||
|
||||
309
session/registry.go
Normal file
309
session/registry.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"tunnel_pls/types"
|
||||
)
|
||||
|
||||
type Key = types.SessionKey
|
||||
|
||||
type Registry interface {
|
||||
Get(key Key) (session *SSHSession, err error)
|
||||
GetWithUser(user string, key Key) (session *SSHSession, err error)
|
||||
Update(user string, oldKey, newKey Key) error
|
||||
Register(key Key, session *SSHSession) (success bool)
|
||||
Remove(key Key)
|
||||
GetAllSessionFromUser(user string) []*SSHSession
|
||||
}
|
||||
type registry struct {
|
||||
mu sync.RWMutex
|
||||
byUser map[string]map[Key]*SSHSession
|
||||
slugIndex map[Key]string
|
||||
}
|
||||
|
||||
func NewRegistry() Registry {
|
||||
return ®istry{
|
||||
byUser: make(map[string]map[Key]*SSHSession),
|
||||
slugIndex: make(map[Key]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registry) Get(key Key) (session *SSHSession, err error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
userID, ok := r.slugIndex[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("session not found")
|
||||
}
|
||||
|
||||
client, ok := r.byUser[userID][key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("session not found")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (r *registry) GetWithUser(user string, key Key) (session *SSHSession, err error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
client, ok := r.byUser[user][key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("session not found")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (r *registry) Update(user string, oldKey, newKey Key) error {
|
||||
if oldKey.Type != newKey.Type {
|
||||
return fmt.Errorf("tunnel type cannot change")
|
||||
}
|
||||
|
||||
if newKey.Type != types.HTTP {
|
||||
return fmt.Errorf("non http tunnel cannot change slug")
|
||||
}
|
||||
|
||||
if isForbiddenSlug(newKey.Id) {
|
||||
return fmt.Errorf("this subdomain is reserved. Please choose a different one")
|
||||
}
|
||||
|
||||
if !isValidSlug(newKey.Id) {
|
||||
return fmt.Errorf("invalid subdomain. Follow the rules")
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, exists := r.slugIndex[newKey]; exists && newKey != oldKey {
|
||||
return fmt.Errorf("someone already uses this subdomain")
|
||||
}
|
||||
client, ok := r.byUser[user][oldKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("session not found")
|
||||
}
|
||||
|
||||
delete(r.byUser[user], oldKey)
|
||||
delete(r.slugIndex, oldKey)
|
||||
|
||||
client.slugManager.Set(newKey.Id)
|
||||
r.slugIndex[newKey] = user
|
||||
|
||||
if r.byUser[user] == nil {
|
||||
r.byUser[user] = make(map[Key]*SSHSession)
|
||||
}
|
||||
r.byUser[user][newKey] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registry) Register(key Key, session *SSHSession) (success bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, exists := r.slugIndex[key]; exists {
|
||||
return false
|
||||
}
|
||||
|
||||
userID := session.lifecycle.GetUser()
|
||||
if r.byUser[userID] == nil {
|
||||
r.byUser[userID] = make(map[Key]*SSHSession)
|
||||
}
|
||||
|
||||
r.byUser[userID][key] = session
|
||||
r.slugIndex[key] = 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(key Key) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
userID, ok := r.slugIndex[key]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
delete(r.byUser[userID], key)
|
||||
if len(r.byUser[userID]) == 0 {
|
||||
delete(r.byUser, userID)
|
||||
}
|
||||
delete(r.slugIndex, key)
|
||||
}
|
||||
|
||||
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,109 +21,121 @@ type Session interface {
|
||||
}
|
||||
|
||||
type SSHSession struct {
|
||||
Lifecycle lifecycle.SessionLifecycle
|
||||
Interaction interaction.Controller
|
||||
Forwarder forwarder.ForwardingController
|
||||
SlugManager slug.Manager
|
||||
|
||||
channelOnce sync.Once
|
||||
initialReq <-chan *ssh.Request
|
||||
sshReqChannel <-chan ssh.NewChannel
|
||||
lifecycle lifecycle.SessionLifecycle
|
||||
interaction interaction.Controller
|
||||
forwarder forwarder.ForwardingController
|
||||
slugManager slug.Manager
|
||||
registry Registry
|
||||
}
|
||||
|
||||
func New(conn *ssh.ServerConn, forwardingReq <-chan *ssh.Request, sshChan <-chan ssh.NewChannel) {
|
||||
func (s *SSHSession) GetLifecycle() lifecycle.SessionLifecycle {
|
||||
return s.lifecycle
|
||||
}
|
||||
|
||||
func (s *SSHSession) GetInteraction() interaction.Controller {
|
||||
return s.interaction
|
||||
}
|
||||
|
||||
func (s *SSHSession) GetForwarder() forwarder.ForwardingController {
|
||||
return s.forwarder
|
||||
}
|
||||
|
||||
func (s *SSHSession) GetSlugManager() slug.Manager {
|
||||
return s.slugManager
|
||||
}
|
||||
|
||||
func New(conn *ssh.ServerConn, forwardingReq <-chan *ssh.Request, sshChan <-chan ssh.NewChannel, sessionRegistry Registry, user 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, user)
|
||||
|
||||
interactionManager.SetLifecycle(lifecycleManager)
|
||||
interactionManager.SetSlugModificator(updateClientSlug)
|
||||
forwarderManager.SetLifecycle(lifecycleManager)
|
||||
lifecycleManager.SetUnregisterClient(unregisterClient)
|
||||
interactionManager.SetSessionRegistry(sessionRegistry)
|
||||
lifecycleManager.SetSessionRegistry(sessionRegistry)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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, err := channel.Accept()
|
||||
if err != nil {
|
||||
log.Printf("failed to accept channel: %v", err)
|
||||
continue
|
||||
}
|
||||
session.channelOnce.Do(func() {
|
||||
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.lifecycle.GetUser(),
|
||||
Active: s.lifecycle.IsActive(),
|
||||
StartedAt: s.lifecycle.StartedAt(),
|
||||
}
|
||||
}
|
||||
|
||||
go session.HandleGlobalRequest(reqs)
|
||||
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 := session.Lifecycle.Close(); err != nil {
|
||||
if err := s.lifecycle.Close(); err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func updateClientSlug(oldSlug, newSlug string) bool {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
if _, exists := Clients[newSlug]; exists && newSlug != oldSlug {
|
||||
return false
|
||||
return fmt.Errorf("no forwarding Request")
|
||||
}
|
||||
|
||||
client, ok := Clients[oldSlug]
|
||||
if !ok {
|
||||
return false
|
||||
s.lifecycle.SetChannel(ch)
|
||||
s.interaction.SetChannel(ch)
|
||||
|
||||
s.HandleTCPIPForward(tcpipReq)
|
||||
|
||||
if err := s.lifecycle.Close(); err != nil {
|
||||
log.Printf("failed to close session: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
delete(Clients, oldSlug)
|
||||
client.SlugManager.Set(newSlug)
|
||||
Clients[newSlug] = client
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerClient(slug string, session *SSHSession) bool {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
if _, exists := Clients[slug]; exists {
|
||||
return false
|
||||
func (s *SSHSession) waitForTCPIPForward() *ssh.Request {
|
||||
select {
|
||||
case req, ok := <-s.initialReq:
|
||||
if !ok {
|
||||
log.Println("Forwarding request channel closed")
|
||||
return nil
|
||||
}
|
||||
if req.Type == "tcpip-forward" {
|
||||
return req
|
||||
}
|
||||
if err := req.Reply(false, nil); err != nil {
|
||||
log.Printf("Failed to reply to request: %v", err)
|
||||
}
|
||||
log.Printf("Expected tcpip-forward request, got: %s", req.Type)
|
||||
return nil
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
log.Println("No forwarding request received")
|
||||
return nil
|
||||
}
|
||||
|
||||
Clients[slug] = session
|
||||
return true
|
||||
}
|
||||
|
||||
func unregisterClient(slug string) {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
delete(Clients, slug)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
package slug
|
||||
|
||||
import "sync"
|
||||
|
||||
type Manager interface {
|
||||
Get() string
|
||||
Set(slug string)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
slug string
|
||||
slugMu sync.RWMutex
|
||||
slug string
|
||||
}
|
||||
|
||||
func NewManager() Manager {
|
||||
return &manager{
|
||||
slug: "",
|
||||
slugMu: sync.RWMutex{},
|
||||
slug: "",
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ const (
|
||||
TCP TunnelType = "TCP"
|
||||
)
|
||||
|
||||
type SessionKey struct {
|
||||
Id string
|
||||
Type TunnelType
|
||||
}
|
||||
|
||||
var BadGatewayResponse = []byte("HTTP/1.1 502 Bad Gateway\r\n" +
|
||||
"Content-Length: 11\r\n" +
|
||||
"Content-Type: text/plain\r\n\r\n" +
|
||||
|
||||
@@ -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