Compare commits

...

69 Commits

Author SHA1 Message Date
Shizun Ge 5d27ee12f8 Merge pull request #214 from shizunge/dependabot/github_actions/actions/checkout-7
Bump actions/checkout from 6 to 7
2026-06-22 12:37:19 -07:00
dependabot[bot] 637aa0fcc4 Bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 08:15:08 +00:00
Shizun Ge bb02fb0180 Merge pull request #213 from adamanteye/main
[dashboard] Filter on cluster, namespace, and job
2026-06-11 12:23:47 -07:00
Shizun Ge 9100b14abf Merge pull request #209 from shizunge/dependabot/go_modules/github.com/pires/go-proxyproto-0.12.0
Bump github.com/pires/go-proxyproto from 0.11.0 to 0.12.0
2026-06-06 18:47:58 -07:00
Shizun Ge b7fdabf81e Merge branch 'main' into dependabot/go_modules/github.com/pires/go-proxyproto-0.12.0 2026-06-06 18:38:24 -07:00
Shizun Ge 9b9c174b84 Merge pull request #211 from shizunge/dependabot/github_actions/docker/login-action-4.2.0
Bump docker/login-action from 4.1.0 to 4.2.0
2026-06-06 18:35:41 -07:00
Shizun Ge bc43eca7de Merge pull request #210 from shizunge/dependabot/github_actions/docker/setup-buildx-action-4.1.0
Bump docker/setup-buildx-action from 4.0.0 to 4.1.0
2026-06-06 18:35:28 -07:00
Shizun Ge a059852463 Merge pull request #208 from shizunge/dependabot/go_modules/github.com/pierrre/geohash-1.1.4
Bump github.com/pierrre/geohash from 1.1.3 to 1.1.4
2026-06-06 18:35:00 -07:00
Shizun Ge 8b180ee911 Merge pull request #212 from shizunge/dependabot/github_actions/docker/build-push-action-7.2.0
Bump docker/build-push-action from 7.0.0 to 7.2.0
2026-06-06 18:34:44 -07:00
Xuelin Yang 2fd6c7a407 [dashboard] Filter on cluster, namespace, and job 2026-06-06 16:40:20 +08:00
dependabot[bot] eddce4b85a Bump docker/build-push-action from 7.0.0 to 7.2.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.0.0 to 7.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v7.0.0...v7.2.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 12:03:40 +00:00
dependabot[bot] 591ad1be81 Bump docker/login-action from 4.1.0 to 4.2.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v4.1.0...v4.2.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 12:03:34 +00:00
dependabot[bot] 2c421593a3 Bump docker/setup-buildx-action from 4.0.0 to 4.1.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 12:03:31 +00:00
dependabot[bot] 15e848bcd7 Bump github.com/pires/go-proxyproto from 0.11.0 to 0.12.0
Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.11.0 to 0.12.0.
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: github.com/pires/go-proxyproto
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 08:50:06 +00:00
dependabot[bot] d2fc221cb2 Bump github.com/pierrre/geohash from 1.1.3 to 1.1.4
Bumps [github.com/pierrre/geohash](https://github.com/pierrre/geohash) from 1.1.3 to 1.1.4.
- [Commits](https://github.com/pierrre/geohash/compare/v1.1.3...v1.1.4)

---
updated-dependencies:
- dependency-name: github.com/pierrre/geohash
  dependency-version: 1.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 08:31:55 +00:00
Shizun Ge 816b060a1c Merge pull request #205 from shizunge/dependabot/github_actions/docker/login-action-4.1.0
Bump docker/login-action from 4.0.0 to 4.1.0
2026-04-07 09:25:46 -07:00
dependabot[bot] 9db64900e0 Bump docker/login-action from 4.0.0 to 4.1.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 08:20:18 +00:00
Shizun Ge 8631efdc1c Merge pull request #204 from shizunge/dependabot/go_modules/github.com/pires/go-proxyproto-0.11.0
Bump github.com/pires/go-proxyproto from 0.8.1 to 0.11.0
2026-03-30 10:37:49 -07:00
dependabot[bot] 4d9fe71b39 Bump github.com/pires/go-proxyproto from 0.8.1 to 0.11.0
Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.8.1 to 0.11.0.
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.8.1...v0.11.0)

---
updated-dependencies:
- dependency-name: github.com/pires/go-proxyproto
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 08:24:34 +00:00
Shizun Ge f0045d32fc [dashboard] Change map circle fill opacity to 0.8 2026-03-28 22:27:46 -07:00
Shizun Ge 3da6dd03b3 [workflows] fix integration tests. 2026-03-26 21:58:14 -07:00
Shizun Ge cc3e0539ff update go to 1.26.1 2026-03-26 21:38:35 -07:00
Shizun Ge 5b3b5973c3 Merge pull request #203 from adamanteye/main
examples/kustomize-simple: add k8s manifests
2026-03-23 21:58:11 -07:00
Xuelin Yang 4bc342b136 examples/kustomize-simple: readonly root filesystem 2026-03-24 12:03:34 +08:00
Xuelin Yang 66244e80a7 examples: sort examples alphabetically 2026-03-24 10:34:19 +08:00
Xuelin Yang 16a32d06cd examples/kustomize-simple: add k8s manifests 2026-03-23 21:42:31 +08:00
Shizun Ge 1e5f0a29ea Merge pull request #202 from BenjaminGoehry/test/prometheus_test
test: add prometheus metrics integration test
2026-03-21 22:09:28 -07:00
Ben G 4344bfbde4 test: add prometheus metrics integration test 2026-03-20 20:42:59 +01:00
Shizun Ge f6e922f36f Merge pull request #196 from DarkWolfCave/fix/ghost-connections
Add TCP keepalive and write deadline on accepted connections
2026-03-17 15:47:54 -07:00
darkwolf cf6fbb5f41 use min(interval, 30s) for TCP keepalive period
Adapt keepalive to the configured interval: for short intervals
(e.g. 10s) detection is faster, for long intervals (e.g. 10min)
it caps at 30s as a safety net. Add comments explaining what
problem keepalive solves and its detection time limitation.
2026-03-17 06:40:43 +01:00
darkwolf ac07a37754 remove unnecessary panic recovery in send goroutine
There is no concrete panic path in the send flow — Write() and
SetWriteDeadline() return errors, not panics. The defer/recover
was overly defensive and could mask real bugs.
2026-03-12 10:56:53 +01:00
darkwolf b6b3fe2678 refactor: use interval as write deadline instead of separate parameter
Simplify the ghost connection fix by reusing the existing interval
duration as the write deadline. This removes the need for a separate
-write_deadline_ms flag while maintaining the same protection against
goroutine leaks from dead connections.
2026-03-12 09:26:38 +01:00
Shizun Ge 3832d95f14 Merge pull request #197 from shizunge/dependabot/github_actions/docker/build-push-action-7.0.0
Bump docker/build-push-action from 6.19.2 to 7.0.0
2026-03-11 21:40:18 -07:00
Shizun Ge 33129ba155 Merge pull request #198 from shizunge/dependabot/github_actions/docker/login-action-4.0.0
Bump docker/login-action from 3.7.0 to 4.0.0
2026-03-11 21:40:09 -07:00
Shizun Ge 7d5eed824d Merge pull request #199 from shizunge/dependabot/github_actions/docker/metadata-action-6
Bump docker/metadata-action from 5 to 6
2026-03-11 21:40:03 -07:00
Shizun Ge 43859e73f2 Merge pull request #200 from shizunge/dependabot/github_actions/docker/setup-qemu-action-4
Bump docker/setup-qemu-action from 3 to 4
2026-03-11 21:39:57 -07:00
Shizun Ge 005145242c Merge pull request #201 from shizunge/dependabot/github_actions/docker/setup-buildx-action-4.0.0
Bump docker/setup-buildx-action from 3.12.0 to 4.0.0
2026-03-11 21:39:49 -07:00
dependabot[bot] a677b35f05 Bump docker/setup-buildx-action from 3.12.0 to 4.0.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.12.0 to 4.0.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.12.0...v4.0.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:27:16 +00:00
dependabot[bot] 760b9cfb6b Bump docker/setup-qemu-action from 3 to 4
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:27:11 +00:00
dependabot[bot] 7bfa69cba0 Bump docker/metadata-action from 5 to 6
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:27:07 +00:00
dependabot[bot] 35b2cb887c Bump docker/login-action from 3.7.0 to 4.0.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3.7.0...v4.0.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:27:01 +00:00
dependabot[bot] b6200c5030 Bump docker/build-push-action from 6.19.2 to 7.0.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.19.2...v7.0.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:26:57 +00:00
Shizun Ge ca1d51b594 Merge pull request #194 from shizunge/dependabot/github_actions/actions/setup-go-6
Bump actions/setup-go from 5 to 6
2026-03-02 21:15:42 -08:00
Shizun Ge 9292291cbd Merge pull request #193 from shizunge/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 4 to 6
2026-03-02 21:15:25 -08:00
Shizun Ge 62a54396cd Merge pull request #192 from BenjaminGoehry/fix/integration_test
fix: integration test concurrency
2026-03-02 21:15:02 -08:00
Ben G ebbcd539fc fix: integration test concurrency
increase timeouts

clean

fix

fix

fix
2026-03-02 21:33:46 +01:00
darkwolf ecdfc514d0 Fix ghost connection goroutine leak via write deadline and TCP keepalive
Connections closed by the kernel but not detected by endlessh-go cause
goroutines to run indefinitely, drifting open/closed Prometheus counters.
This happens because conn.Write() succeeds on dead connections when the
kernel buffers data. Kernel 6.12 (Debian 13) is more aggressive at
buffering, making ghosts permanent rather than self-healing.

Changes:
- Add SetWriteDeadline before conn.Write to detect dead connections
- Enable TCP keepalive (30s) on accepted connections for kernel-level
  dead peer detection
- Add defer/recover in send goroutine for robustness
- Add -write_deadline_ms flag (default 30000, 0 to disable)

No new dependencies - uses only Go stdlib net package functions.
2026-03-02 19:14:07 +01:00
dependabot[bot] b088bdbd15 Bump actions/setup-go from 5 to 6
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 08:28:14 +00:00
dependabot[bot] 5a88627724 Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 08:28:09 +00:00
Shizun Ge 3fd26b15db Merge pull request #188 from BenjaminGoehry/test/integration_test
test: add integration tests for multi-port, tarpit behavior, and max_clients limit
2026-02-23 21:28:21 -08:00
Ben G df8978a8f1 review 2026-02-24 00:03:26 +01:00
Shizun Ge 1133f6ad28 Merge pull request #191 from shizunge/dependabot/github_actions/docker/build-push-action-6.19.2
Bump docker/build-push-action from 6.18.0 to 6.19.2
2026-02-18 20:43:29 -08:00
dependabot[bot] 869e25828b Bump docker/build-push-action from 6.18.0 to 6.19.2
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.18.0 to 6.19.2.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.18.0...v6.19.2)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.19.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 08:25:03 +00:00
Ben G eea67a33b1 fix server for random port and review 2026-02-09 23:38:27 +01:00
Shizun Ge cfb21bf1c5 Merge pull request #189 from shizunge/dependabot/github_actions/docker/login-action-3.7.0
Bump docker/login-action from 3.6.0 to 3.7.0
2026-02-05 22:21:39 -08:00
dependabot[bot] 3d6365caf0 Bump docker/login-action from 3.6.0 to 3.7.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3.6.0...v3.7.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 08:39:53 +00:00
Ben G 5fa58afc0b fix 2026-01-29 22:16:32 +01:00
Ben G ac6cda3e73 update github workflow with test job 2026-01-29 22:14:45 +01:00
Ben G f48f6d748f test(integration): add multiple ports behavior, tarpit heavior check and concurrency respecting max_clients 2026-01-28 14:12:15 +01:00
Shizun Ge 0d5395b4ee Update copyright to 2026. 2026-01-05 20:07:30 -08:00
Shizun Ge 1585df7876 Merge pull request #187 from shizunge/dependabot/github_actions/docker/setup-buildx-action-3.12.0
Bump docker/setup-buildx-action from 3.11.1 to 3.12.0
2025-12-24 22:32:46 -08:00
dependabot[bot] 07df006502 Bump docker/setup-buildx-action from 3.11.1 to 3.12.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.11.1 to 3.12.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.11.1...v3.12.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 3.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 08:01:45 +00:00
Shizun Ge 829fe080a8 Merge pull request #186 from shizunge/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-11-24 10:44:01 -08:00
dependabot[bot] 63b07abf45 Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 08:02:13 +00:00
Shizun Ge b4077f7002 [dashboard] Add ports selection. 2025-11-07 00:34:28 -08:00
Shizun Ge f64550176c Merge pull request #184 from shizunge/dependabot/github_actions/docker/login-action-3.6.0
Bump docker/login-action from 3.5.0 to 3.6.0
2025-10-06 10:47:04 -07:00
Shizun Ge 3c60cdf2ed Merge pull request #185 from shizunge/dependabot/github_actions/peter-evans/dockerhub-description-5
Bump peter-evans/dockerhub-description from 4 to 5
2025-10-06 10:46:56 -07:00
dependabot[bot] 5e3b96cc65 Bump peter-evans/dockerhub-description from 4 to 5
Bumps [peter-evans/dockerhub-description](https://github.com/peter-evans/dockerhub-description) from 4 to 5.
- [Release notes](https://github.com/peter-evans/dockerhub-description/releases)
- [Commits](https://github.com/peter-evans/dockerhub-description/compare/v4...v5)

---
updated-dependencies:
- dependency-name: peter-evans/dockerhub-description
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 08:02:22 +00:00
dependabot[bot] f6e2189c79 Bump docker/login-action from 3.5.0 to 3.6.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3.5.0...v3.6.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 08:02:19 +00:00
20 changed files with 623 additions and 86 deletions
+2 -2
View File
@@ -12,9 +12,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Update Docker Hub description
uses: peter-evans/dockerhub-description@v4
uses: peter-evans/dockerhub-description@v5
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
+23 -5
View File
@@ -15,19 +15,37 @@ env:
PLATFORMS: "linux/amd64,linux/arm64,linux/arm/v7"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.26.1'
cache: true
- name: Install dependencies
run: go mod download
- name: Run Tests
run: go test -v ./...
build_container_image:
name: Build Docker image
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@v4.1.0
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
ghcr.io/${{ github.repository }}-development
@@ -37,7 +55,7 @@ jobs:
type=ref,event=branch
type=edge,branch=main
- name: Build
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@v7.2.0
with:
platforms: ${{ env.PLATFORMS }}
push: false
+24 -7
View File
@@ -16,31 +16,48 @@ env:
PLATFORMS: "linux/amd64,linux/arm64,linux/arm/v7"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.26.1'
cache: true
- name: Install dependencies
run: go mod download
- name: Run Tests
run: go test -v ./...
build_and_push:
name: Build and push Docker image
runs-on: ubuntu-latest
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@v4.1.0
- name: Login to Docker Hub
uses: docker/login-action@v3.5.0
uses: docker/login-action@v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.5.0
uses: docker/login-action@v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ github.repository }}-development
@@ -51,7 +68,7 @@ jobs:
type=ref,event=branch
type=edge,branch=main
- name: Build and push ${{ github.repository }}:${{ steps.git.outputs.image_tag }}
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@v7.2.0
with:
platforms: ${{ env.PLATFORMS }}
push: true
+7 -7
View File
@@ -13,25 +13,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@v4.1.0
- name: Login to docker hub
uses: docker/login-action@v3.5.0
uses: docker/login-action@v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.5.0
uses: docker/login-action@v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ github.repository }}
@@ -40,7 +40,7 @@ jobs:
type=ref,event=branch
type=ref,event=tag
- name: Build and push
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@v7.2.0
with:
platforms: ${{ env.PLATFORMS }}
push: true
+22 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2021-2024 Shizun Ge
// Copyright (C) 2021-2026 Shizun Ge
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -54,6 +54,23 @@ func NewClient(conn net.Conn, interval time.Duration, maxClients int64) *Client
time.Sleep(interval)
}
atomic.AddInt64(&numCurrentClients, 1)
// Enable TCP keepalive to detect dead peers at the kernel level.
// This is a safety net for long intervals where the write deadline
// alone would be too slow to detect dead connections. For example,
// with interval=10min, without keepalive a ghost connection would
// go undetected for 10+ minutes.
// Keepalive cannot detect peers that are alive at the TCP level
// (kernel still responds to probes) but stalled at the application
// level — the write deadline in Send() covers that case.
// Detection time: ~idle + 9 probes (e.g. 10s + 9×10s ≈ 100s on Linux).
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
keepalive := interval
if keepalive > 30*time.Second {
keepalive = 30 * time.Second
}
tcpConn.SetKeepAlivePeriod(keepalive)
}
addr := conn.RemoteAddr().(*net.TCPAddr)
glog.V(1).Infof("ACCEPT host=%v port=%v n=%v/%v\n", addr.IP, addr.Port, numCurrentClients, maxClients)
return &Client{
@@ -80,6 +97,10 @@ func (c *Client) Send(bannerMaxLength int64) (int, error) {
}
c.next = time.Now().Add(c.interval)
length := rand.Int63n(bannerMaxLength)
// Set a write deadline to detect dead connections where the kernel
// buffers data but the remote peer is gone. Without this, Write()
// can succeed indefinitely on dead connections, causing goroutine leaks.
c.conn.SetWriteDeadline(time.Now().Add(c.interval))
bytesSent, err := c.conn.Write(randStringBytes(length))
if err != nil {
return 0, err
+73 -42
View File
@@ -21,7 +21,7 @@
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "10.3.0-64399"
"version": "12.3.0-18356121373.patch9"
},
{
"type": "panel",
@@ -76,10 +76,9 @@
}
]
},
"description": "Dashboard for endlessh (Fix current connections)",
"description": "Dashboard for endlessh (Add ports selection)",
"editable": false,
"fiscalYearStartMonth": 0,
"gnetId": 15156,
"graphTooltip": 0,
"id": null,
"links": [
@@ -108,7 +107,6 @@
"url": "https://grafana.com/grafana/dashboards/15156"
}
],
"liveNow": false,
"panels": [
{
"datasource": {
@@ -126,7 +124,7 @@
"steps": [
{
"color": "green",
"value": null
"value": 0
}
]
}
@@ -147,6 +145,7 @@
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
@@ -154,11 +153,12 @@
"fields": "/^Total number connections that endlessh trapped$/",
"values": false
},
"showPercentChange": false,
"text": {},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "10.3.0-64399",
"pluginVersion": "12.3.0-18356121373.patch9",
"targets": [
{
"datasource": {
@@ -166,7 +166,7 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "(endlessh_client_open_count{instance=~\"$host\",job=~\"$job\"} - endlessh_client_open_count{instance=~\"$host\",job=~\"$job\"} offset $__interval) > 0 or (endlessh_client_open_count{instance=~\"$host\",job=~\"$job\"}!=0 unless endlessh_client_open_count{instance=~\"$host\",job=~\"$job\"} offset $__interval)",
"expr": "(endlessh_client_open_count{instance=~\"$host\", local_port=~\"$port\",job=~\"$job\"} - endlessh_client_open_count{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"} offset $__interval) > 0 or (endlessh_client_open_count{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"}!=0 unless endlessh_client_open_count{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"} offset $__interval)",
"format": "table",
"instant": false,
"legendFormat": "Seen {{ip}}",
@@ -179,7 +179,7 @@
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "(endlessh_client_trapped_time_seconds{instance=~\"$host\",job=~\"$job\"} - endlessh_client_trapped_time_seconds{instance=~\"$host\",job=~\"$job\"} offset $__interval) > 0 or (endlessh_client_trapped_time_seconds{instance=~\"$host\",job=~\"$job\"}!=0 unless endlessh_client_trapped_time_seconds{instance=~\"$host\",job=~\"$job\"} offset $__interval)",
"expr": "(endlessh_client_trapped_time_seconds{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"} - endlessh_client_trapped_time_seconds{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"} offset $__interval) > 0 or (endlessh_client_trapped_time_seconds{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"}!=0 unless endlessh_client_trapped_time_seconds{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"} offset $__interval)",
"format": "table",
"hide": false,
"instant": false,
@@ -271,7 +271,7 @@
"steps": [
{
"color": "green",
"value": null
"value": 0
}
]
},
@@ -291,6 +291,7 @@
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
@@ -298,11 +299,12 @@
"fields": "/^Time spent on endlessh$/",
"values": false
},
"showPercentChange": false,
"text": {},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "10.3.0-64399",
"pluginVersion": "12.3.0-18356121373.patch9",
"targets": [
{
"datasource": {
@@ -397,7 +399,7 @@
"steps": [
{
"color": "green",
"value": null
"value": 0
}
]
},
@@ -417,6 +419,7 @@
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
@@ -424,22 +427,25 @@
"fields": "",
"values": false
},
"showPercentChange": false,
"text": {},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "10.3.0-64399",
"pluginVersion": "12.3.0-18356121373.patch9",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum(increase(endlessh_sent_bytes_total{instance=~\"$host\",job=~\"$job\"}[$__range]))",
"expr": "sum(increase(endlessh_sent_bytes_total{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"}[$__range]))",
"hide": false,
"interval": "",
"legendFormat": "Bytes sent by endlessh",
"range": true,
"refId": "sent_bytes"
}
],
@@ -462,7 +468,7 @@
"steps": [
{
"color": "green",
"value": null
"value": 0
}
]
}
@@ -481,6 +487,7 @@
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
@@ -488,11 +495,12 @@
"fields": "/^Unique IPs connected$/",
"values": false
},
"showPercentChange": false,
"text": {},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "10.3.0-64399",
"pluginVersion": "12.3.0-18356121373.patch9",
"targets": [
{
"datasource": {
@@ -576,7 +584,7 @@
"steps": [
{
"color": "green",
"value": null
"value": 0
}
]
}
@@ -595,6 +603,7 @@
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
@@ -602,11 +611,12 @@
"fields": "/^Client IP of the latest connection$/",
"values": false
},
"showPercentChange": false,
"text": {},
"textMode": "value",
"wideLayout": true
},
"pluginVersion": "10.3.0-64399",
"pluginVersion": "12.3.0-18356121373.patch9",
"targets": [
{
"datasource": {
@@ -712,7 +722,7 @@
"steps": [
{
"color": "green",
"value": null
"value": 0
},
{
"color": "#EAB839",
@@ -740,6 +750,7 @@
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
@@ -747,19 +758,21 @@
"fields": "",
"values": false
},
"showPercentChange": false,
"text": {},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "10.3.0-64399",
"pluginVersion": "12.3.0-18356121373.patch9",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum((endlessh_client_open_count_total{instance=~\"$host\",job=~\"$job\"}) - (endlessh_client_closed_count_total{instance=~\"$host\",job=~\"$job\"} or endlessh_client_open_count_total{instance=~\"$host\",job=~\"$job\"} * 0))",
"expr": "sum((endlessh_client_open_count_total{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"}) - (endlessh_client_closed_count_total{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"} or endlessh_client_open_count_total{instance=~\"$host\",local_port=~\"$port\",job=~\"$job\"} * 0))",
"instant": false,
"interval": "",
"legendFormat": "Open Connections",
@@ -786,6 +799,7 @@
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
@@ -802,6 +816,7 @@
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
@@ -812,13 +827,13 @@
}
},
"mappings": [],
"min": -0.01,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"value": 0
}
]
}
@@ -840,10 +855,12 @@
"showLegend": false
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.3.0-18356121373.patch9",
"targets": [
{
"datasource": {
@@ -904,11 +921,14 @@
"fields": "",
"values": false
},
"sort": "desc",
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.3.0-18356121373.patch9",
"targets": [
{
"datasource": {
@@ -1015,7 +1035,7 @@
"steps": [
{
"color": "#96D98D",
"value": null
"value": 0
}
]
}
@@ -1029,7 +1049,6 @@
"y": 7
},
"id": 48,
"links": [],
"options": {
"basemap": {
"config": {},
@@ -1051,7 +1070,7 @@
"field": "Connections",
"fixed": "dark-green"
},
"fillOpacity": 0.4,
"fillOpacity": 0.8,
"shape": "circle",
"showLegend": false,
"size": {
@@ -1094,10 +1113,11 @@
"id": "zero",
"lat": 0,
"lon": 0,
"noRepeat": false,
"zoom": 1
}
},
"pluginVersion": "10.3.0-64399",
"pluginVersion": "12.3.0-18356121373.patch9",
"targets": [
{
"datasource": {
@@ -1194,6 +1214,9 @@
"type": "auto"
},
"filterable": true,
"footer": {
"reducers": []
},
"inspect": false,
"minWidth": 50
},
@@ -1204,7 +1227,7 @@
"steps": [
{
"color": "green",
"value": null
"value": 0
}
]
}
@@ -1271,14 +1294,6 @@
"id": 49,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"frameIndex": 0,
"showHeader": true,
"sortBy": [
@@ -1288,7 +1303,7 @@
}
]
},
"pluginVersion": "10.3.0-64399",
"pluginVersion": "12.3.0-18356121373.patch9",
"targets": [
{
"datasource": {
@@ -1461,7 +1476,7 @@
}
],
"refresh": "",
"schemaVersion": 39,
"schemaVersion": 42,
"tags": [
"prometheus"
],
@@ -1475,7 +1490,6 @@
"uid": "${DS_PROMETHEUS}"
},
"definition": "label_values(endlessh_client_open_count_total, job)",
"hide": 0,
"includeAll": true,
"label": "Job",
"multi": true,
@@ -1487,7 +1501,6 @@
},
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
},
@@ -1499,7 +1512,6 @@
"uid": "${DS_PROMETHEUS}"
},
"definition": "label_values(endlessh_client_open_count_total{job=~\"$job\"}, instance)",
"hide": 0,
"includeAll": true,
"label": "Host",
"multi": true,
@@ -1511,7 +1523,26 @@
},
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
},
{
"allValue": ".*",
"current": {},
"definition": "label_values(endlessh_client_open_count_total{job=~\"$job\", instance=~\"$host\"},local_port)",
"description": "",
"includeAll": true,
"label": "Port",
"multi": true,
"name": "port",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(endlessh_client_open_count_total{job=~\"$job\", instance=~\"$host\"},local_port)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 2,
"regex": "",
"sort": 1,
"type": "query"
}
@@ -1525,6 +1556,6 @@
"timezone": "",
"title": "Endlessh",
"uid": "ATIxYkO7k",
"version": 12,
"version": 13,
"weekStart": ""
}
+324
View File
@@ -0,0 +1,324 @@
package main
import (
"bytes"
"fmt"
"net"
"os/exec"
"regexp"
"strings"
"sync"
"testing"
"time"
)
const (
waitForListenTimeout = 10 * time.Second
waitForConnectTimeout = 3 * time.Second
pollInterval = 50 * time.Millisecond
)
func waitForLogMatch(stderr *bytes.Buffer, pattern string, timeout time.Duration) bool {
re := regexp.MustCompile(pattern)
deadline := time.Now().Add(timeout)
for {
if time.Now().After(deadline) {
return false
}
if re.MatchString(stderr.String()) {
return true
}
time.Sleep(pollInterval)
}
}
func TestEndlesshIntegration_MultiplePorts(t *testing.T) {
const nPorts = 3
ports := make([]int, nPorts)
for i := 0; i < nPorts; i++ {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to get free port: %v", err)
}
ports[i] = ln.Addr().(*net.TCPAddr).Port
ln.Close()
}
args := []string{"run", "main.go",
"-interval_ms=100",
"-max_clients=10",
"-logtostderr",
"-v=1",
}
for _, p := range ports {
args = append(args, fmt.Sprintf("-port=%d", p))
}
cmd := exec.Command("go", args...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer func() {
if err := cmd.Process.Kill(); err != nil {
t.Logf("Failed to kill process: %v", err)
}
}()
if !waitForLogMatch(&stderr, "Listening on", waitForListenTimeout) {
t.Fatalf("Timeout waiting for server to start, got logs: %s", stderr.String())
}
for _, port := range ports {
addr := fmt.Sprintf("localhost:%d", port)
conn, err := net.Dial("tcp", addr)
if err != nil {
t.Fatalf("Failed to connect to server on port %d: %v", port, err)
}
if !waitForLogMatch(&stderr, `ACCEPT host=(127\.0\.0\.1|::1)`, waitForConnectTimeout) {
t.Errorf("Never saw any ACCEPT log, got logs: %s", stderr.String())
}
conn.Close()
if !waitForLogMatch(&stderr, `CLOSE host=(127\.0\.0\.1|::1)`, waitForConnectTimeout) {
t.Errorf("Never saw any CLOSE log, got logs: %s", stderr.String())
}
}
}
func TestEndlesshIntegration_TarpitBehavior(t *testing.T) {
var stderr bytes.Buffer
cmd := exec.Command("go", "run", "main.go", "-port=0", "-interval_ms=5000", "-max_clients=10", "-logtostderr", "-v=1")
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer func() {
if err := cmd.Process.Kill(); err != nil {
t.Logf("Failed to kill process: %v", err)
}
}()
if !waitForLogMatch(&stderr, "Listening on", waitForListenTimeout) {
t.Fatalf("Timeout waiting for server to start, got logs: %s", stderr.String())
}
stderrOutput := stderr.String()
re := regexp.MustCompile(`Listening on .*:(\d+)`)
m := re.FindStringSubmatch(stderrOutput)
if len(m) != 2 {
t.Fatalf("Could not parse port from logs: %s", stderrOutput)
}
port := m[1]
addr := "localhost:" + port
conn, err := net.Dial("tcp", addr)
if err != nil {
t.Fatalf("Failed to connect to server on port %s: %v", port, err)
}
defer conn.Close()
// Simulate SSH client banner
// Connect & send client banner
_, err = conn.Write([]byte("SSH-2.0-OpenSSH_8.2p1\r\n"))
if err != nil {
t.Fatalf("Write failed: %v", err)
}
// Expect FIRST LINE immediately (no delay)
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil || n == 0 {
t.Fatalf("Expected first tarpit line immediately, got: %v (%d bytes)", err, n)
}
t.Logf("Got first tarpit line (%d bytes): %q", n, buf[:n])
time.Sleep(100 * time.Millisecond)
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
n2, err := conn.Read(buf)
if err == nil && n2 > 0 {
t.Errorf("Got %d bytes too soon (within 500ms), tarpit failed", n2)
}
}
func TestEndlesshIntegration_Concurrency(t *testing.T) {
maxClients := 5
var stderr bytes.Buffer
cmd := exec.Command("go", "run", "main.go", "-port=0", "-interval_ms=1000", fmt.Sprintf("-max_clients=%d", maxClients), "-logtostderr", "-v=1")
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer func() {
if err := cmd.Process.Kill(); err != nil {
t.Logf("Failed to kill process: %v", err)
}
}()
if !waitForLogMatch(&stderr, "Listening on", waitForListenTimeout) {
t.Fatalf("Timeout waiting for server to start, got logs: %s", stderr.String())
}
stderrOutput := stderr.String()
re := regexp.MustCompile(`Listening on .*:(\d+)`)
m := re.FindStringSubmatch(stderrOutput)
if len(m) != 2 {
t.Fatalf("Could not parse port from logs: %s", stderrOutput)
}
port := m[1]
addr := fmt.Sprintf("localhost:%s", port)
// Test multiple connections
var wg sync.WaitGroup
var mu sync.Mutex
activeClients := 0
maxActiveClients := 0
//sixthClientFailed := false
successfulReads := 0
for i := 0; i < maxClients+1; i++ {
wg.Add(1)
go func(clientID int) {
defer wg.Done()
conn, dialErr := net.Dial("tcp", addr)
if dialErr != nil {
if clientID == maxClients {
//sixthClientFailed = true
t.Logf("Client %d dial failed (expected): %v", clientID, dialErr)
} else {
t.Errorf("Client %d dial failed (unexpected): %v", clientID, dialErr)
}
return
}
defer conn.Close()
_, writeErr := conn.Write([]byte("SSH-2.0-OpenSSH_8.2p1\r\n"))
if writeErr != nil {
t.Logf("Client %d write failed: %v", clientID, writeErr)
return
}
buf := make([]byte, 1024)
conn.SetReadDeadline(time.Now().Add(3 * time.Second))
n1, readErr1 := conn.Read(buf)
if readErr1 != nil || n1 == 0 {
if clientID != maxClients { // only log unexpected failures
t.Logf("Client %d failed first tarpit line (unexpected): %v (%d bytes)", clientID, readErr1, n1)
}
return
}
// Client is active only if it successfully received data
mu.Lock()
activeClients++
if activeClients > maxActiveClients {
maxActiveClients = activeClients
}
successfulReads++
mu.Unlock()
t.Logf("Client %d got line 1 (%d bytes): %q", clientID, n1, buf[:n1])
// Keep the connection open for a while
time.Sleep(5 * time.Second)
mu.Lock()
activeClients--
mu.Unlock()
}(i)
time.Sleep(200 * time.Millisecond)
}
wg.Wait()
mu.Lock()
deferredSuccessfulReads := successfulReads
mu.Unlock()
if deferredSuccessfulReads < 1 {
t.Errorf("Expected at least one client to receive a tarpit line, got %d", deferredSuccessfulReads)
}
// Check if maxActiveClients exceeded maxClients
if maxActiveClients > maxClients {
t.Errorf("Expected max %d concurrent clients, got %d", maxClients, maxActiveClients)
}
// Check if the 6th client failed
/*mu.Lock()
if !sixthClientFailed {
t.Errorf("Expected 6th client to fail, but it succeeded")
}
mu.Unlock()
*/
// The 6th client does not necessarily fail at Dial because max_clients is
// enforced at the protocol/Read stage instead of at Accept.
//
// TODO: Enforce max_clients at Accept so the N+1 client fails fast at Dial.
}
func TestEndlesshIntegration_PrometheusMetrics(t *testing.T) {
var stderr bytes.Buffer
cmd := exec.Command(
"go", "run", "main.go",
"-port=0",
"-enable_prometheus",
"-prometheus_port=0",
"-interval_ms=100",
"-logtostderr", "-v=1",
)
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer cmd.Process.Kill()
if !waitForLogMatch(&stderr, "Starting Prometheus", waitForListenTimeout) {
t.Fatalf("Prometheus did not start: %s", stderr.String())
}
reProm := regexp.MustCompile(`Prometheus on IP port .*:(\d+)`)
reMain := regexp.MustCompile(`Listening on .*:(\d+)`)
promMatch := reProm.FindStringSubmatch(stderr.String())
mainMatch := reMain.FindStringSubmatch(stderr.String())
if len(promMatch) < 2 || len(mainMatch) < 2 {
t.Fatalf("Could not parse ports: %s", stderr.String())
}
promPort := promMatch[1]
mainPort := mainMatch[1]
conn, err := net.Dial("tcp", "localhost:"+mainPort)
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
conn.Write([]byte("SSH-2.0-test\r\n"))
time.Sleep(500 * time.Millisecond)
// Fetch metrics
resp, err := net.Dial("tcp", "localhost:"+promPort)
if err != nil {
t.Fatalf("Failed to connect to metrics endpoint: %v", err)
}
fmt.Fprintf(resp, "GET /metrics HTTP/1.1\r\nHost: localhost\r\n\r\n")
buf := make([]byte, 8192)
n, _ := resp.Read(buf)
body := string(buf[:n])
if !strings.Contains(body, "endlessh_client_open_count_total") {
t.Errorf("Missing expected metric in output:\n%s", body)
}
if !strings.Contains(body, "endlessh_sent_bytes_total") {
t.Errorf("Expected bytes metric not found:\n%s", body)
}
}
+5
View File
@@ -10,7 +10,12 @@ An example how to setup endlessh-go, Prometheus, and Grafana using [docker compo
An example how to setup endlessh-go with the Maxmind GeoIP Database.
## [kustomize-simple](./kustomize-simple)
An example how to setup endlessh-go using [kustomize](https://kustomize.io/).
## FAQ
### Bind to privileged ports (<1024) in a container
You need to add capability `NET_BIND_SERVICE` to the program.
+25
View File
@@ -0,0 +1,25 @@
## kustomize
This is an example how to setup endlessh-go with existing Prometheus and Grafana using [kustomize](https://kustomize.io/).
This example assumes the cluster already has a Prometheus Operator based monitoring stack. It deploys:
- endlessh-go
- a Service exposing SSH and Prometheus metrics
- a `ServiceMonitor` for scraping endlessh-go metrics
- a Grafana dashboard `ConfigMap`
To deploy the stack, run:
```bash
kubectl apply -k examples/kustomize-simple
```
`dashboard.json` is added to a `ConfigMap` with label `grafana_dashboard=1`, which can be picked up by a Grafana sidecar based dashboard loader.
The `ServiceMonitor` in `monitor.yaml` scrapes the `metrics` port every `60s`. If your Prometheus stack only selects `ServiceMonitor` objects with specific labels, add the matching label in `kustomization.yaml`.
The `endlessh` Service exposes the following ports inside the cluster:
- **22**: SSH for endlessh-go
- **2112**: Prometheus metrics exported by endlessh-go
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: endlessh
spec:
replicas: 1
template:
spec:
automountServiceAccountToken: false
containers:
- name: endlessh
image: shizunge/endlessh-go:latest
args:
- -interval_ms=1000
- -logtostderr
- -v=1
- -enable_prometheus
- -geoip_supplier=ip-api
- -host=[::]
- -prometheus_host=[::]
ports:
- name: ssh
containerPort: 2222
- name: metrics
containerPort: 2112
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
@@ -0,0 +1,21 @@
resources:
- deployment.yaml
- service.yaml
- monitor.yaml
configMapGenerator:
- name: endlessh-dashboard
files:
- dashboard.json
options:
labels:
grafana_dashboard: "1"
labels:
- pairs:
app.kubernetes.io/name: endlessh
includeSelectors: true
includeTemplates: true
fields:
- group: monitoring.coreos.com
kind: ServiceMonitor
path: spec/selector/matchLabels
create: true
+10
View File
@@ -0,0 +1,10 @@
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: endlessh
spec:
endpoints:
- interval: 60s
path: /metrics
port: metrics
jobLabel: app.kubernetes.io/name
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: endlessh
spec:
ports:
- name: ssh
port: 22
targetPort: ssh
protocol: TCP
- name: metrics
port: 2112
targetPort: metrics
protocol: TCP
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2023-2024 Shizun Ge
// Copyright (C) 2023-2026 Shizun Ge
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2021-2024 Shizun Ge
// Copyright (C) 2021-2026 Shizun Ge
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
+3 -3
View File
@@ -1,11 +1,12 @@
module endlessh-go
go 1.24.2
go 1.26.1
require (
github.com/golang/glog v1.2.5
github.com/oschwald/geoip2-golang v1.13.0
github.com/pierrre/geohash v1.1.3
github.com/pierrre/geohash v1.1.4
github.com/pires/go-proxyproto v0.12.0
github.com/prometheus/client_golang v1.23.2
)
@@ -15,7 +16,6 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
+12 -12
View File
@@ -33,18 +33,18 @@ github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNs
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
github.com/pierrre/assert v0.9.0 h1:eIKXsqcLSeLAOXYGHreen2D5CTZ2/N0/cJBNdxuVLdM=
github.com/pierrre/assert v0.9.0/go.mod h1:3tthe4L3xYU4biRPVTFo9t2YRO4Dg3+zrLyMS4YanCE=
github.com/pierrre/compare v1.4.13 h1:b6gi3OgN1emmD1Ly37m+B/Pbq6tac+w3lNGT5xu4I10=
github.com/pierrre/compare v1.4.13/go.mod h1:+ie0ecM2nS32oLck0FWDstwIUSZ0YF4KBIaACOvKhJM=
github.com/pierrre/geohash v1.1.3 h1:3u+EbHm2FZQnZCu3E2SaeryIQYtA/eH1YYzDpFm/42c=
github.com/pierrre/geohash v1.1.3/go.mod h1:K5UlVmtRxicTXgp6eShrlAOk2Neu9zOe76C/ug7RIZ8=
github.com/pierrre/go-libs v0.17.0 h1:bjxd9unioV/YDkUW7obETp2IFct0kO9HePURn81UL8s=
github.com/pierrre/go-libs v0.17.0/go.mod h1:920odOqc5mZREW9GFWg056mjQ2prNVRGUZO7HRS2Jlc=
github.com/pierrre/pretty v0.14.3 h1:I100hHs1C/MCd3M0D/hIV7J2OXl7amLD0uP2jnB7mRw=
github.com/pierrre/pretty v0.14.3/go.mod h1:HTaFDNtT9ELVK5pODLfXRLiEiyIx3MmQUL5UadrR3/0=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pierrre/assert v0.12.1 h1:Hg+r10601guGiOvXoWHx1Xp2TLj3w6FMb+PN7pR+iQc=
github.com/pierrre/assert v0.12.1/go.mod h1:9AEGGFuJp339PdEUkhfOq8zqYPdSurPWAua35EEpY2o=
github.com/pierrre/compare v1.4.14 h1:snH4S/2nClsCCgYAvseyQYX6RHX8WvCdd23deiL1ovc=
github.com/pierrre/compare v1.4.14/go.mod h1:5RkfHZojsCAFZR+mW9SHH55mH1tCWNWMP7xhWgxc6QE=
github.com/pierrre/geohash v1.1.4 h1:yzKGZnvToMhSorOzdR7dULzYkzc6BgEYN//yAn/sWsw=
github.com/pierrre/geohash v1.1.4/go.mod h1:acEkGYRKiLkhsU88AolKimmPzyBC7vqi9YUs0/LkKNI=
github.com/pierrre/go-libs v0.28.1 h1:PCz1fdKdSwCmwO5GMCN/L5qlvqxIBT54iKKzrbI1Nik=
github.com/pierrre/go-libs v0.28.1/go.mod h1:KXbqq+niFCQ48LPSjX2lKG1IdCoTBrTpurkVVDH2W8U=
github.com/pierrre/pretty v0.23.0 h1:LYCkefa+DV87cu/xqUnPh75x1T0zXgnHvAn7wW+OgN0=
github.com/pierrre/pretty v0.23.0/go.mod h1:ILYWjIqXAO9iLC2sDcHkgv2Vp0JIIKGMjRtwHAdOVAM=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+23 -4
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2021-2024 Shizun Ge
// Copyright (C) 2021-2026 Shizun Ge
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -24,6 +24,7 @@ import (
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
@@ -70,7 +71,14 @@ func startSending(maxClients int64, bannerMaxLength int64, records chan<- metric
func startAccepting(maxClients int64, connType, connHost, connPort string, interval time.Duration, clients chan<- *client.Client, records chan<- metrics.RecordEntry, proxyProtocolEnabled bool, proxyProtocolReadHeaderTimeout int) {
go func() {
l, err := net.Listen(connType, connHost+":"+connPort)
connPortInt, err := strconv.Atoi(connPort)
if err != nil {
glog.Errorf("Invalid port: %v", err)
os.Exit(1)
}
addr := connHost + fmt.Sprintf(":%d", connPortInt)
l, err := net.Listen(connType, addr)
if err != nil {
glog.Errorf("Error listening: %v", err)
os.Exit(1)
@@ -84,7 +92,9 @@ func startAccepting(maxClients int64, connType, connHost, connPort string, inter
// Close the listener when the application closes.
defer l.Close()
glog.Infof("Listening on %v:%v", connHost, connPort)
realAddr := l.Addr().(*net.TCPAddr)
localPortStr := strconv.Itoa(realAddr.Port)
glog.Infof("Listening on %v:%v", connHost, realAddr.Port)
for {
// Listen for an incoming connection.
conn, err := l.Accept()
@@ -97,7 +107,7 @@ func startAccepting(maxClients int64, connType, connHost, connPort string, inter
records <- metrics.RecordEntry{
RecordType: metrics.RecordEntryTypeStart,
IpAddr: remoteIpAddr,
LocalPort: connPort,
LocalPort: localPortStr,
}
clients <- c
}
@@ -146,6 +156,15 @@ func main() {
if *connType == "tcp6" && *prometheusHost == "0.0.0.0" {
*prometheusHost = "[::]"
}
if *prometheusPort == "0" || *prometheusPort == "" {
l, err := net.Listen("tcp", *prometheusHost+":0")
if err != nil {
glog.Fatalf("Failed to pick a free Prometheus port: %v", err)
}
actualPort := l.Addr().(*net.TCPAddr).Port
*prometheusPort = strconv.Itoa(actualPort)
l.Close()
}
metrics.InitPrometheus(*prometheusHost, *prometheusPort, *prometheusEntry)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2024 Shizun Ge
// Copyright (C) 2024-2026 Shizun Ge
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by