From 83657d32064de083cdf0ce7cb554dd9635fa637a Mon Sep 17 00:00:00 2001 From: bagas Date: Sun, 28 Dec 2025 18:45:22 +0700 Subject: [PATCH 1/6] refactor: remove unnecessary caching of environment data --- utils/utils.go | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/utils/utils.go b/utils/utils.go index d2087d1..52637be 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -11,22 +11,18 @@ import ( "path/filepath" "strconv" "strings" - "sync" "time" "github.com/joho/godotenv" "golang.org/x/crypto/ssh" ) -type Env struct { - value map[string]string - mu sync.Mutex -} - -var env *Env - func init() { - env = &Env{value: map[string]string{}} + 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 GenerateRandomString(length int) string { @@ -41,24 +37,10 @@ func GenerateRandomString(length int) string { } func Getenv(key, defaultValue string) string { - env.mu.Lock() - defer env.mu.Unlock() - if val, ok := env.value[key]; ok { - return val - } - - if os.Getenv("HOSTNAME") == "" { - err := godotenv.Load(".env") - if err != nil { - log.Fatalf("Error loading .env file: %s", err) - } - } - val := os.Getenv(key) if val == "" { val = defaultValue } - env.value[key] = val return val } -- 2.49.1 From 1d918ef2aa98d6c77d783959cadb5f79465a8dbe Mon Sep 17 00:00:00 2001 From: bagas Date: Sun, 28 Dec 2025 19:03:26 +0700 Subject: [PATCH 2/6] feat(port): disable TCP forwarding by default and refactor port manager --- internal/port/port.go | 47 +++++++++++++++++++++------------- session/handler.go | 12 ++++----- session/lifecycle/lifecycle.go | 2 +- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/internal/port/port.go b/internal/port/port.go index 6512f40..68e185a 100644 --- a/internal/port/port.go +++ b/internal/port/port.go @@ -9,36 +9,47 @@ import ( "tunnel_pls/utils" ) -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", "40000-41000") + rawRange := utils.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() diff --git a/session/handler.go b/session/handler.go index eb61cfd..d536b51 100644 --- a/session/handler.go +++ b/session/handler.go @@ -107,7 +107,7 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) { return } else { if portToBind == 0 { - unassign, success := portUtil.Manager.GetUnassignedPort() + unassign, success := portUtil.Default.GetUnassignedPort() portToBind = unassign if !success { s.Interaction.SendMessage("No available port\r\n") @@ -122,7 +122,7 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) { } return } - } else if isUse, isExist := portUtil.Manager.GetPortStatus(portToBind); isExist && isUse { + } else if isUse, isExist := portUtil.Default.GetPortStatus(portToBind); isExist && isUse { s.Interaction.SendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port. (03)\r\n", portToBind)) err := req.Reply(false, nil) if err != nil { @@ -135,7 +135,7 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) { } return } - err := portUtil.Manager.SetPortStatus(portToBind, true) + err := portUtil.Default.SetPortStatus(portToBind, true) if err != nil { log.Println("Failed to set port status:", err) return @@ -208,7 +208,7 @@ func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, 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)) - if setErr := portUtil.Manager.SetPortStatus(portToBind, false); setErr != nil { + if setErr := portUtil.Default.SetPortStatus(portToBind, false); setErr != nil { log.Printf("Failed to reset port status: %v", setErr) } err = req.Reply(false, nil) @@ -227,7 +227,7 @@ func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind err = binary.Write(buf, binary.BigEndian, uint32(portToBind)) if err != nil { log.Println("Failed to write port to buffer:", err) - if setErr := portUtil.Manager.SetPortStatus(portToBind, false); setErr != nil { + if setErr := portUtil.Default.SetPortStatus(portToBind, false); setErr != nil { log.Printf("Failed to reset port status: %v", setErr) } err = listener.Close() @@ -242,7 +242,7 @@ 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) - if setErr := portUtil.Manager.SetPortStatus(portToBind, false); setErr != nil { + if setErr := portUtil.Default.SetPortStatus(portToBind, false); setErr != nil { log.Printf("Failed to reset port status: %v", setErr) } err = listener.Close() diff --git a/session/lifecycle/lifecycle.go b/session/lifecycle/lifecycle.go index ecfc206..11106f8 100644 --- a/session/lifecycle/lifecycle.go +++ b/session/lifecycle/lifecycle.go @@ -85,7 +85,7 @@ func (l *Lifecycle) Close() error { } if l.Forwarder.GetTunnelType() == types.TCP { - err := portUtil.Manager.SetPortStatus(l.Forwarder.GetForwardedPort(), false) + err := portUtil.Default.SetPortStatus(l.Forwarder.GetForwardedPort(), false) if err != nil { return err } -- 2.49.1 From eee04daf80a884c3247b2635be9005c4e01af480 Mon Sep 17 00:00:00 2001 From: bagas Date: Sun, 28 Dec 2025 19:29:32 +0700 Subject: [PATCH 3/6] feat: optimize Docker build for production --- .dockerignore | 30 ++++++++++++++++++++++++++++++ Dockerfile | 42 ++++++++++++++++++++++++++++++++++-------- go.sum | 37 ++++++++++++++++++------------------- 3 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3c51983 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +.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/ + diff --git a/Dockerfile b/Dockerfile index 484427a..e7a8104 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,46 @@ FROM golang:1.25.5-alpine AS go_builder +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 GOARCH=amd64 \ + go build -trimpath \ + -ldflags="-w -s" \ + -o /app/tunnel_pls \ + . + +RUN adduser -D -u 10001 -g '' appuser FROM scratch -WORKDIR /src - 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 /app/tunnel_pls /app/tunnel_pls -ENV TZ Asia/Jakarta +WORKDIR /app -ENTRYPOINT ["./main"] \ No newline at end of file +USER appuser + +ENV TZ=Asia/Jakarta + +EXPOSE 2200 80 443 + +LABEL org.opencontainers.image.title="Tunnel Please" \ + org.opencontainers.image.description="SSH-based tunnel server" + +ENTRYPOINT ["/app/tunnel_pls"] diff --git a/go.sum b/go.sum index faee6f1..fb988d1 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,10 @@ github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx6 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/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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -14,44 +18,39 @@ github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc= github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= -golang.org/x/crypto v0.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/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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.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/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -- 2.49.1 From c3a469be641b9d082e2d20d8837bd13f5f4aa856 Mon Sep 17 00:00:00 2001 From: bagas Date: Sun, 28 Dec 2025 19:53:03 +0700 Subject: [PATCH 4/6] refactor: use relative paths for certificates instead of absolute paths --- .dockerignore | 2 ++ .gitignore | 1 + Dockerfile | 6 ++++-- README.md | 14 ++++++++------ main.go | 2 +- server/tls.go | 6 +++--- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.dockerignore b/.dockerignore index 3c51983..c28dce0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,3 +28,5 @@ renovate-config.js *_test.go testdata/ +app + diff --git a/.gitignore b/.gitignore index bfc3046..dc40a4f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ id_rsa* .env tmp certs +app \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e7a8104..7f34cd1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,9 @@ RUN --mount=type=cache,target=/go/pkg/mod \ -o /app/tunnel_pls \ . -RUN adduser -D -u 10001 -g '' appuser +RUN adduser -D -u 10001 -g '' appuser && \ + mkdir -p /app/certs/ssh /app/certs/tls && \ + chown -R appuser:appuser /app FROM scratch @@ -30,7 +32,7 @@ COPY --from=go_builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=go_builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=go_builder /etc/passwd /etc/passwd COPY --from=go_builder /etc/group /etc/group -COPY --from=go_builder /app/tunnel_pls /app/tunnel_pls +COPY --from=go_builder --chown=appuser:appuser /app /app WORKDIR /app diff --git a/README.md b/README.md index 7a52b04..69bd86e 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,9 @@ The following environment variables can be configured in the `.env` file: | `PORT` | SSH server port | `2200` | No | | `TLS_ENABLED` | Enable TLS/HTTPS | `false` | No | | `TLS_REDIRECT` | Redirect HTTP to HTTPS | `false` | No | -| `CERT_LOC` | Path to TLS certificate | `certs/cert.pem` | No | -| `KEY_LOC` | Path to TLS private key | `certs/privkey.pem` | No | -| `CERT_STORAGE_PATH` | Path for CertMagic certificate storage | `certs/certmagic` | No | | `ACME_EMAIL` | Email for Let's Encrypt registration | `admin@` | 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 | -| `SSH_PRIVATE_KEY` | Path to SSH private key (auto-generated if missing) | `certs/id_rsa` | No | | `CORS_LIST` | Comma-separated list of allowed CORS origins | - | No | | `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 | @@ -43,8 +39,14 @@ The following environment variables can be configured in the `.env` file: The server supports automatic TLS certificate generation and renewal using [CertMagic](https://github.com/caddyserver/certmagic) with Cloudflare DNS-01 challenge. This is required for wildcard certificate support (`*.yourdomain.com`). +**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 (`CERT_LOC`, `KEY_LOC`) exist and cover both `DOMAIN` and `*.DOMAIN`, they will be used +1. If user-provided certificates exist at `certs/tls/cert.pem` and `certs/tls/privkey.pem` and cover both `DOMAIN` and `*.DOMAIN`, they will be used 2. If certificates are missing, expired, expiring within 30 days, or don't cover the required domains, CertMagic will automatically obtain new certificates from Let's Encrypt 3. Certificates are automatically renewed before expiration 4. User-provided certificates support hot-reload (changes detected every 30 seconds) @@ -71,7 +73,7 @@ ACME_EMAIL=admin@example.com ### SSH Key Auto-Generation -If the SSH private key specified in `SSH_PRIVATE_KEY` doesn't exist, the application will automatically generate a new 4096-bit RSA key pair at the specified location. This makes it easier to get started without manually creating SSH keys. +The application will automatically generate a new 4096-bit RSA key pair at `certs/ssh/id_rsa` if it doesn't exist. This makes it easier to get started without manually creating SSH keys. SSH keys are stored separately from TLS certificates. ### Memory Optimization diff --git a/main.go b/main.go index 65fb3ca..0d90318 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ func main() { ServerVersion: "SSH-2.0-TunnlPls-1.0", } - sshKeyPath := utils.Getenv("SSH_PRIVATE_KEY", "certs/id_rsa") + sshKeyPath := "certs/ssh/id_rsa" if err := utils.GenerateSSHKeyIfNotExist(sshKeyPath); err != nil { log.Fatalf("Failed to generate SSH key: %s", err) } diff --git a/server/tls.go b/server/tls.go index e5b3105..1eb6ac8 100644 --- a/server/tls.go +++ b/server/tls.go @@ -37,9 +37,9 @@ func NewTLSConfig(domain string) (*tls.Config, error) { var initErr error tlsManagerOnce.Do(func() { - certPath := utils.Getenv("CERT_LOC", "certs/cert.pem") - keyPath := utils.Getenv("KEY_LOC", "certs/privkey.pem") - storagePath := utils.Getenv("CERT_STORAGE_PATH", "certs/certmagic") + certPath := "certs/tls/cert.pem" + keyPath := "certs/tls/privkey.pem" + storagePath := "certs/tls/certmagic" tm := &TLSManager{ domain: domain, -- 2.49.1 From bf7f7bd8da1b628030b7d0c844adb5e493d9cef1 Mon Sep 17 00:00:00 2001 From: bagas Date: Sun, 28 Dec 2025 20:03:49 +0700 Subject: [PATCH 5/6] feat: add configurable HTTPS port --- Dockerfile | 2 +- README.md | 1 + server/https.go | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7f34cd1..45e31af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ USER appuser ENV TZ=Asia/Jakarta -EXPOSE 2200 80 443 +EXPOSE 2200 80 8443 LABEL org.opencontainers.image.title="Tunnel Please" \ org.opencontainers.image.description="SSH-based tunnel server" diff --git a/README.md b/README.md index 69bd86e..e1b8e26 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The following environment variables can be configured in the `.env` file: |----------|-------------|---------|----------| | `DOMAIN` | Domain name for subdomain routing | `localhost` | No | | `PORT` | SSH server port | `2200` | 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@` | No | diff --git a/server/https.go b/server/https.go index 2964d4f..fc08424 100644 --- a/server/https.go +++ b/server/https.go @@ -14,13 +14,14 @@ import ( func NewHTTPSServer() error { domain := utils.Getenv("DOMAIN", "localhost") + httpsPort := utils.Getenv("HTTPS_PORT", "8443") tlsConfig, err := NewTLSConfig(domain) if err != nil { return fmt.Errorf("failed to initialize TLS config: %w", err) } - ln, err := tls.Listen("tcp", ":443", tlsConfig) + ln, err := tls.Listen("tcp", ":"+httpsPort, tlsConfig) if err != nil { return err } -- 2.49.1 From b5862bd7a092cfeccbddbdc0ab2ba4883fccf5ab Mon Sep 17 00:00:00 2001 From: bagas Date: Sun, 28 Dec 2025 20:09:31 +0700 Subject: [PATCH 6/6] feat: add configurable HTTP port --- Dockerfile | 2 +- README.md | 1 + server/http.go | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 45e31af..83d9b84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ USER appuser ENV TZ=Asia/Jakarta -EXPOSE 2200 80 8443 +EXPOSE 2200 8080 8443 LABEL org.opencontainers.image.title="Tunnel Please" \ org.opencontainers.image.description="SSH-based tunnel server" diff --git a/README.md b/README.md index e1b8e26..c7ddf33 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The following environment variables can be configured in the `.env` file: |----------|-------------|---------|----------| | `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 | diff --git a/server/http.go b/server/http.go index 6c716d1..0ca4e23 100644 --- a/server/http.go +++ b/server/http.go @@ -193,7 +193,8 @@ func (cw *CustomWriter) AddInteraction(interaction Interaction) { var redirectTLS = false func NewHTTPServer() error { - listener, err := net.Listen("tcp", ":80") + httpPort := utils.Getenv("HTTP_PORT", "8080") + listener, err := net.Listen("tcp", ":"+httpPort) if err != nil { return errors.New("Error listening: " + err.Error()) } -- 2.49.1