Compare commits

...

90 Commits

Author SHA1 Message Date
dependabot[bot]
cba2a08db6
build(deps): bump github.com/docker/docker (#332) 2024-09-23 18:50:04 -05:00
dependabot[bot]
377f7d33bc
build(deps): bump github.com/docker/docker in the patches group (#328) 2024-09-15 22:02:04 -05:00
dependabot[bot]
d93564670f
build(deps): bump github.com/docker/docker (#326) 2024-09-09 07:56:04 -05:00
dependabot[bot]
5402099fdb
build(deps): bump golang.org/x/text from 0.17.0 to 0.18.0 (#327) 2024-09-09 07:50:48 -05:00
Geoff Bourne
cab6ba1e71
Trim serverAddress before logging its value for lookup (#321) 2024-08-15 19:31:15 -05:00
dependabot[bot]
39bdcfc3cc
build(deps): bump golang.org/x/text from 0.16.0 to 0.17.0 (#319) 2024-08-11 22:07:42 -05:00
csabca83
33d0c5c56a
Update documentation - autoscaling requirements (#317) 2024-08-06 12:35:57 -05:00
Matthew McCune
5d7063f73e
Add Service Discovery option for non-Swarm Docker (#316) 2024-08-04 17:22:39 -05:00
dependabot[bot]
8b3180d852
build(deps): bump github.com/docker/docker (#315) 2024-07-29 07:40:14 -05:00
dependabot[bot]
beae573d81
build(deps): bump golang.ngrok.com/ngrok from 1.9.1 to 1.10.0 (#310) 2024-07-14 19:52:29 -05:00
dependabot[bot]
2a643f71ab
build(deps): bump github.com/docker/docker in the patches group (#309) 2024-07-07 19:40:43 -05:00
Geoff Bourne
ed7ee144ad
build: upgrade to Go 1.22.5 (#308) 2024-07-07 11:22:26 -05:00
iipanda
e38a054c46
feat: add ability to receive proxy protocol (#307) 2024-07-07 11:13:12 -05:00
dependabot[bot]
f32dfa3800
build(deps): bump github.com/docker/docker in the patches group (#306) 2024-07-01 08:31:51 -05:00
dependabot[bot]
2c2ddabf2d
build(deps): bump github.com/docker/docker (#305) 2024-06-16 22:05:55 -05:00
dependabot[bot]
d68b141c66
build(deps): bump golang.org/x/text from 0.15.0 to 0.16.0 (#301) 2024-06-15 14:20:25 -05:00
dependabot[bot]
732283d1c5
build(deps): bump github.com/docker/docker from 26.1.3+incompatible to 26.1.4+incompatible in the patches group (#300)
* build(deps): bump github.com/docker/docker in the patches group

Bumps the patches group with 1 update: [github.com/docker/docker](https://github.com/docker/docker).


Updates `github.com/docker/docker` from 26.1.3+incompatible to 26.1.4+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.1.3...v26.1.4)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump to Go 1.22.4

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Geoff Bourne <itzgeoff@gmail.com>
2024-06-15 14:04:35 -05:00
dependabot[bot]
ff1709ef3b
build(deps): bump github.com/docker/docker from 26.1.1+incompatible to 26.1.2+incompatible in the patches group (#292)
* build(deps): bump github.com/docker/docker in the patches group

Bumps the patches group with 1 update: [github.com/docker/docker](https://github.com/docker/docker).


Updates `github.com/docker/docker` from 26.1.1+incompatible to 26.1.2+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.1.1...v26.1.2)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump to Go 1.22.3

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Geoff Bourne <itzgeoff@gmail.com>
2024-05-24 21:30:30 -05:00
dependabot[bot]
28a78380d0
build(deps): bump github.com/docker/docker from 26.1.1+incompatible to 26.1.3+incompatible in the patches group across 1 directory (#293)
* build(deps): bump github.com/docker/docker

Bumps the patches group with 1 update in the / directory: [github.com/docker/docker](https://github.com/docker/docker).


Updates `github.com/docker/docker` from 26.1.1+incompatible to 26.1.3+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.1.1...v26.1.3)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump to Go 1.22.3

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Geoff Bourne <itzgeoff@gmail.com>
2024-05-24 21:15:10 -05:00
dependabot[bot]
e17d5d6188
build(deps): bump github.com/docker/docker in the patches group (#289) 2024-05-06 08:05:34 -05:00
dependabot[bot]
5f79ad653e
build(deps): bump golang.org/x/text from 0.14.0 to 0.15.0 (#290) 2024-05-06 07:28:35 -05:00
Ambrose Chua
f34b6e85b0
Prevent crash due to large memory allocation (#291) 2024-05-05 21:10:23 -05:00
dependabot[bot]
09ed21d467
build(deps): bump github.com/docker/docker (#288) 2024-04-29 07:42:23 -05:00
dependabot[bot]
c4d2918053
build(deps): bump github.com/docker/docker from 26.0.0+incompatible to 26.0.2+incompatible (#284)
* build(deps): bump github.com/docker/docker

Bumps [github.com/docker/docker](https://github.com/docker/docker) from 26.0.0+incompatible to 26.0.2+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.0.0...v26.0.2)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Add toolchain to go.mod

* Bump to go 1.22.2

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Geoff Bourne <itzgeoff@gmail.com>
2024-04-18 19:02:12 -05:00
dependabot[bot]
392c64f7fb
build(deps): bump github.com/docker/docker (#281)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 25.0.5+incompatible to 26.0.0+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v25.0.5...v26.0.0)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-24 20:55:24 -05:00
dependabot[bot]
e15c0d8110
build(deps): bump github.com/docker/docker (#280) 2024-03-20 16:13:55 -05:00
dependabot[bot]
3cc4f3c861
build(deps): bump the patches group with 1 update (#276) 2024-03-17 22:09:52 -05:00
dependabot[bot]
e441a72d90
build(deps): bump google.golang.org/protobuf from 1.32.0 to 1.33.0 (#275)
Bumps google.golang.org/protobuf from 1.32.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 14:30:15 -05:00
dependabot[bot]
8ca88f416e
build(deps): bump the patches group with 1 update (#274) 2024-03-11 08:20:48 -05:00
dependabot[bot]
b762c1b04e
build(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#273) 2024-03-04 07:58:53 -06:00
dependabot[bot]
705ffbf093
build(deps): bump golang.ngrok.com/ngrok from 1.8.1 to 1.9.0 (#272) 2024-03-03 18:11:53 -06:00
Stein van Broekhoven
5dc576fc2e
auto scale up and down with many named backends in k8s (#270) 2024-02-19 09:54:24 -06:00
dependabot[bot]
555033b54d
build(deps): bump the patches group with 1 update (#266) 2024-02-18 19:48:55 -06:00
dependabot[bot]
f85b23b271
build(deps): bump the patches group with 1 update (#265) 2024-02-12 12:21:58 -06:00
dependabot[bot]
fad894e773
build(deps): bump the patches group with 1 update (#263) 2024-02-04 19:44:16 -06:00
dependabot[bot]
1d6df79ba3
build(deps): bump golang.ngrok.com/ngrok from 1.7.0 to 1.8.0 (#264)
Bumps [golang.ngrok.com/ngrok](https://github.com/ngrok/ngrok-go) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/ngrok/ngrok-go/releases)
- [Changelog](https://github.com/ngrok/ngrok-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ngrok/ngrok-go/compare/v1.7.0...v1.8.0)

---
updated-dependencies:
- dependency-name: golang.ngrok.com/ngrok
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 18:50:05 -06:00
Geoff Bourne
b2153f89cb
Corrected addresses used in PROXY header (#262) 2024-02-03 19:37:44 -06:00
dependabot[bot]
51b8d44c77
build(deps): bump github.com/docker/docker (#260) 2024-01-30 08:17:23 -06:00
dependabot[bot]
ac3e315a1f
build(deps): bump golang.ngrok.com/ngrok from 1.5.1 to 1.7.0 (#253) 2023-12-25 07:18:54 -07:00
dependabot[bot]
f9572965f9
build(deps): bump github.com/itzg/go-flagsfiller from 1.13.1 to 1.14.0 (#251) 2023-12-24 20:01:20 -07:00
dependabot[bot]
ffacdb5155
build(deps): bump golang.org/x/text from 0.13.0 to 0.14.0 (#246)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.13.0 to 0.14.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-07 21:49:49 -06:00
dependabot[bot]
952e298c87 build(deps): bump the patches group with 1 update
Bumps the patches group with 1 update: [github.com/gorilla/mux](https://github.com/gorilla/mux).

- [Release notes](https://github.com/gorilla/mux/releases)
- [Commits](https://github.com/gorilla/mux/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/gorilla/mux
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 08:58:28 -06:00
dependabot[bot]
aa4b27c8f5
build(deps): bump the patches group with 2 updates (#244)
Bumps the patches group with 2 updates: [k8s.io/api](https://github.com/kubernetes/api) and [k8s.io/client-go](https://github.com/kubernetes/client-go).


Updates `k8s.io/api` from 0.28.2 to 0.28.3
- [Commits](https://github.com/kubernetes/api/compare/v0.28.2...v0.28.3)

Updates `k8s.io/client-go` from 0.28.2 to 0.28.3
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.28.2...v0.28.3)

---
updated-dependencies:
- dependency-name: k8s.io/api
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-03 15:45:03 -05:00
dependabot[bot]
a96cde55d5
build(deps): bump github.com/docker/docker (#243)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.6+incompatible to 24.0.7+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v24.0.6...v24.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-29 22:00:38 -05:00
Geoff Bourne
4f54002231
Allow for MAPPING to be declared with newline delimited entries (#241) 2023-10-28 16:54:46 -05:00
dependabot[bot]
ccc0b6f9f4
build(deps): bump golang.ngrok.com/ngrok from 1.4.1 to 1.5.1 (#238)
Bumps [golang.ngrok.com/ngrok](https://github.com/ngrok/ngrok-go) from 1.4.1 to 1.5.1.
- [Release notes](https://github.com/ngrok/ngrok-go/releases)
- [Changelog](https://github.com/ngrok/ngrok-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ngrok/ngrok-go/compare/v1.4.1...v1.5.1)

---
updated-dependencies:
- dependency-name: golang.ngrok.com/ngrok
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-22 20:33:31 -05:00
dependabot[bot]
1e1428c6bd
build(deps): bump golang.org/x/net from 0.13.0 to 0.17.0 (#236) 2023-10-14 17:27:10 -05:00
Geoff Bourne
2cbb998aff
build: use common workflows (#235)
* build(deps): bump docker/setup-buildx-action from 2 to 3

Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump goreleaser/goreleaser-action from 4 to 5

Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 4 to 5.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump docker/login-action from 2 to 3

Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump k8s.io/apimachinery from 0.28.1 to 0.28.2

Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.28.1 to 0.28.2.
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.28.1...v0.28.2)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump golang.ngrok.com/ngrok from 1.4.0 to 1.4.1

Bumps [golang.ngrok.com/ngrok](https://github.com/ngrok/ngrok-go) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/ngrok/ngrok-go/releases)
- [Changelog](https://github.com/ngrok/ngrok-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ngrok/ngrok-go/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: golang.ngrok.com/ngrok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* build: use common workflows

* build(deps): upgrade k8s

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-23 18:44:30 -05:00
dependabot[bot]
5fb718df2d
build(deps): bump actions/checkout from 3 to 4 (#226) 2023-09-11 22:09:09 -05:00
dependabot[bot]
35aaae5bcb
build(deps): bump github.com/docker/docker (#225) 2023-09-11 07:18:41 -05:00
Geoff Bourne
a73a1689c9
deps: upgrade k8s and others (#224) 2023-09-09 14:11:44 -05:00
dependabot[bot]
b4b97f51df
build(deps): bump github.com/stretchr/testify from 1.8.2 to 1.8.4 (#200)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.2 to 1.8.4.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.2...v1.8.4)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-09 13:35:24 -05:00
dependabot[bot]
84907ef8cd
build(deps): bump github.com/docker/docker (#215)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.2+incompatible to 24.0.5+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v24.0.2...v24.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-09 13:31:17 -05:00
Geoff Bourne
33f393f1b9
ci: fix goreleaser --rm-dist deprecation (#223) 2023-09-09 13:30:53 -05:00
Geoff Bourne
afdca5646e
Added ngrok support (#222) 2023-09-09 12:06:59 -05:00
dependabot[bot]
25601c65a6
build(deps): bump github.com/sirupsen/logrus from 1.9.2 to 1.9.3 (#210) 2023-07-16 20:28:12 -05:00
dependabot[bot]
4435b7cb55
build(deps): bump golang.org/x/text from 0.9.0 to 0.11.0 (#209) 2023-07-09 21:36:13 -05:00
dependabot[bot]
ff7ef24f4c
build(deps): bump github.com/itzg/go-flagsfiller from 1.11.0 to 1.12.0 (#202)
Bumps [github.com/itzg/go-flagsfiller](https://github.com/itzg/go-flagsfiller) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/itzg/go-flagsfiller/releases)
- [Commits](https://github.com/itzg/go-flagsfiller/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: github.com/itzg/go-flagsfiller
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-11 20:20:24 -05:00
dependabot[bot]
302b68400b
build(deps): bump github.com/docker/docker (#201) 2023-06-05 10:10:36 -05:00
dependabot[bot]
f6dc9cd03a
build(deps): bump github.com/sirupsen/logrus from 1.9.0 to 1.9.2 (#198) 2023-05-29 08:54:08 -05:00
dependabot[bot]
88d2e8a013
build(deps): bump github.com/itzg/go-flagsfiller from 1.9.2 to 1.11.0 (#199) 2023-05-29 08:21:48 -05:00
dependabot[bot]
7f4a6843ea
build(deps): bump github.com/docker/distribution (#187) 2023-05-25 06:59:25 -05:00
Geoff Bourne
1803ecfe04
Fixed Go version for test build (#197) 2023-05-24 22:28:41 -05:00
dependabot[bot]
8fab072e18
build(deps): bump golang.org/x/text from 0.8.0 to 0.9.0 (#181) 2023-05-24 22:22:16 -05:00
dependabot[bot]
c046edec8b
build(deps): bump github.com/docker/docker (#191) 2023-05-24 22:21:31 -05:00
Lilly Tempest
c548fda65e
Support TCP Shield host enrinchment (#195) 2023-05-24 21:28:38 -05:00
Geoff Bourne
059ee78c5c
Simplify PR github action (#196) 2023-05-24 21:27:44 -05:00
Lilly Tempest
de655ee432
Add documentation for routes config file (#194) 2023-05-24 08:24:39 -05:00
dependabot[bot]
087a325720
build(deps): bump github.com/docker/docker (#184)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 23.0.1+incompatible to 23.0.4+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v23.0.1...v23.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-23 21:51:25 -05:00
dependabot[bot]
426aebeadb
build(deps): bump github.com/itzg/go-flagsfiller from 1.9.1 to 1.9.2 (#178) 2023-04-09 22:14:29 -05:00
dependabot[bot]
ae24356102
build(deps): bump github.com/pires/go-proxyproto from 0.6.2 to 0.7.0 (#175) 2023-03-20 14:35:16 -05:00
dependabot[bot]
80a9d38471
build(deps): bump actions/setup-go from 3 to 4 (#171) 2023-03-20 07:32:24 -05:00
dependabot[bot]
a3cd1598bf
build(deps): bump golang.org/x/crypto (#165)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.0.0-20210915214749-c084706c2272 to 0.1.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 12:22:44 -06:00
dependabot[bot]
a1067acfa1
build(deps): bump golang.org/x/text from 0.7.0 to 0.8.0 (#167) 2023-03-05 22:25:34 -06:00
dependabot[bot]
e39d4049eb
build(deps): bump github.com/stretchr/testify from 1.8.1 to 1.8.2 (#166) 2023-02-26 19:42:23 -06:00
dependabot[bot]
da057f5b34
build(deps): bump golang.org/x/net (#164) 2023-02-24 20:59:56 -06:00
dependabot[bot]
1f8236ad92
build(deps): bump github.com/docker/docker (#162) 2023-02-14 07:22:23 -06:00
dependabot[bot]
7fb3cbd7bf
build(deps): bump golang.org/x/text from 0.6.0 to 0.7.0 (#163) 2023-02-13 08:38:17 -06:00
dependabot[bot]
dc5e5cac31
build(deps): bump github.com/docker/docker (#160) 2023-02-06 07:21:21 -06:00
dependabot[bot]
137bd9c8ec
build(deps): bump docker/build-push-action from 3.2.0 to 3.3.0 (#155) 2023-01-15 19:02:30 -06:00
dependabot[bot]
4d93eeaa0d
build(deps): bump golang.org/x/text from 0.5.0 to 0.6.0 (#154) 2023-01-08 20:54:54 -06:00
dependabot[bot]
28695b3bba
build(deps): bump github.com/docker/docker (#151)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.21+incompatible to 20.10.22+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v20.10.21...v20.10.22)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-19 20:28:04 -06:00
dependabot[bot]
730a8d3d89
build(deps): bump goreleaser/goreleaser-action from 3 to 4 (#150) 2022-12-18 19:32:08 -06:00
Lasse15
787ab54ca4
Persistent routes from REST API (#145) 2022-12-05 07:58:13 -06:00
dependabot[bot]
e1bb4980bb
build(deps): bump golang.org/x/text from 0.4.0 to 0.5.0 (#146)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.4.0 to 0.5.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.4.0...v0.5.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-04 20:00:04 -06:00
n1xx1
c2b23d40c7
Enable network filtering for docker swarm discovery (#143) 2022-11-17 08:38:44 -06:00
dependabot[bot]
298ea8eef9
build(deps): bump github.com/docker/docker (#138) 2022-10-30 22:04:42 -05:00
dependabot[bot]
7117562215
build(deps): bump github.com/stretchr/testify from 1.8.0 to 1.8.1 (#137) 2022-10-30 19:33:03 -05:00
dependabot[bot]
085bb6e133
build(deps): bump github.com/docker/docker (#134) 2022-10-24 12:52:21 -05:00
Geoff Bourne
a7229355bf
Route to k8s service port minecraft or mc-router (#135) 2022-10-23 22:18:10 -05:00
23 changed files with 1718 additions and 1212 deletions

View File

@ -5,7 +5,20 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
groups:
updates:
patterns:
- "*"
update-types:
- patch
- minor
- package-ecosystem: "gomod" - package-ecosystem: "gomod"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
groups:
patches:
patterns:
- "*"
update-types:
- patch

View File

@ -1,56 +0,0 @@
name: Validate PR
on:
pull_request:
branches: [ master ]
types: [assigned, opened, synchronize, labeled]
env:
IMAGE_TO_TEST: itzg/mc-router:test-${{ github.repository_owner }}-${{ github.run_id }}
jobs:
test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build
uses: docker/build-push-action@v3.2.0
with:
tags: ${{ env.IMAGE_TO_TEST }}
load: true
cache-from: type=gha
target: builder
- name: Run tests
run: |
docker run --rm --entrypoint go ${{ env.IMAGE_TO_TEST }} test ./...
- name: Gather Docker metadata
if: contains(github.event.pull_request.labels.*.name, 'ci/push-image')
id: meta
uses: docker/metadata-action@v4
with:
images: |
itzg/mc-router
- name: Login to DockerHub
if: contains(github.event.pull_request.labels.*.name, 'ci/push-image')
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push
if: contains(github.event.pull_request.labels.*.name, 'ci/push-image')
uses: docker/build-push-action@v3.2.0
with:
tags: ${{ steps.meta.outputs.tags }}
pull: true
push: true
cache-from: type=gha
labels: ${{ steps.meta.outputs.labels }}

View File

@ -8,35 +8,9 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-20.04 uses: itzg/github-workflows/.github/workflows/go-with-releaser-image.yml@main
env: with:
DOCKER_CLI_EXPERIMENTAL: "enabled" go-version: "1.22.5"
steps: secrets:
- name: Checkout image-registry-username: ${{ secrets.DOCKERHUB_USERNAME }}
uses: actions/checkout@v3 image-registry-password: ${{ secrets.DOCKERHUB_TOKEN }}
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Setup docker buildx
uses: docker/setup-buildx-action@v2
with:
install: true
- name: Docker login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,28 +1,14 @@
name: test name: Test
on: on:
push: push:
branches: branches:
- master - master
pull_request:
branches: [ master ]
jobs: jobs:
build: build:
runs-on: ubuntu-20.04 uses: itzg/github-workflows/.github/workflows/go-test.yml@main
with:
steps: go-version: "1.22.5"
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.17
- name: Test
run: go test ./...
- name: Run GoReleaser Snapshot
uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: build --snapshot --rm-dist

View File

@ -1,4 +1,4 @@
FROM golang:1.17 as builder FROM golang:1.22 AS builder
WORKDIR /build WORKDIR /build
@ -6,7 +6,7 @@ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 go build ./cmd/mc-router RUN CGO_ENABLED=0 go build -buildvcs=false ./cmd/mc-router
FROM scratch FROM scratch
ENTRYPOINT ["/mc-router"] ENTRYPOINT ["/mc-router"]

334
README.md
View File

@ -7,116 +7,167 @@
Routes Minecraft client connections to backend servers based upon the requested server address. Routes Minecraft client connections to backend servers based upon the requested server address.
# Usage ## Usage
```text ```text
-api-binding host:port -api-binding host:port
The host:port bound for servicing API requests (env API_BINDING) The host:port bound for servicing API requests (env API_BINDING)
-auto-scale-up -auto-scale-up
Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed (env AUTO_SCALE_UP) Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed (env AUTO_SCALE_UP)
-connection-rate-limit int -connection-rate-limit int
Max number of connections to allow per second (env CONNECTION_RATE_LIMIT) (default 1) Max number of connections to allow per second (env CONNECTION_RATE_LIMIT) (default 1)
-cpu-profile string -cpu-profile string
Enables CPU profiling and writes to given path (env CPU_PROFILE) Enables CPU profiling and writes to given path (env CPU_PROFILE)
-debug -debug
Enable debug logs (env DEBUG) Enable debug logs (env DEBUG)
-in-kube-cluster -default string
Use in-cluster Kubernetes config (env IN_KUBE_CLUSTER) host:port of a default Minecraft server to use when mapping not found (env DEFAULT)
-kube-config string -docker-socket
The path to a Kubernetes configuration file (env KUBE_CONFIG) Path to Docker socket to use (env DOCKER_SOCKET) (default "unix:///var/run/docker.sock")
-docker-refresh-interval int
Refresh interval in seconds for the Docker Swarm integration (env DOCKER_REFRESH_INTERVAL) (default 15)
-docker-timeout int
Timeout configuration in seconds for the Docker Swarm integration (env DOCKER_TIMEOUT)
-in-docker
Use Docker service discovery (env IN_DOCKER)
-in-docker-swarm -in-docker-swarm
Use in-swarm Docker config (env IN_DOCKER_SWARM) Use Docker Swarm service discovery (env IN_DOCKER_SWARM)
-docker-timeout -in-kube-cluster
Timeout configuration in seconds for the Docker Swarm integration (env DOCKER_TIMEOUT) (default 0) Use in-cluster Kubernetes config (env IN_KUBE_CLUSTER)
-docker-refresh-interval -kube-config string
Refresh interval in seconds for the Docker Swarm integration (env DOCKER_REFRESH_INTERVAL) (default 15) The path to a Kubernetes configuration file (env KUBE_CONFIG)
-mapping string -mapping value
Comma-separated mappings of externalHostname=host:port (env MAPPING) Comma-separated or repeated mappings of externalHostname=host:port (env MAPPING)
-metrics-backend string -metrics-backend string
Backend to use for metrics exposure/publishing: discard,expvar,influxdb (env METRICS_BACKEND) (default "discard") Backend to use for metrics exposure/publishing: discard,expvar,influxdb (env METRICS_BACKEND) (default "discard")
-metrics-backend-config-influxdb-addr string -metrics-backend-config-influxdb-addr string
(env METRICS_BACKEND_CONFIG_INFLUXDB_ADDR) (env METRICS_BACKEND_CONFIG_INFLUXDB_ADDR)
-metrics-backend-config-influxdb-database string -metrics-backend-config-influxdb-database string
(env METRICS_BACKEND_CONFIG_INFLUXDB_DATABASE) (env METRICS_BACKEND_CONFIG_INFLUXDB_DATABASE)
-metrics-backend-config-influxdb-interval duration -metrics-backend-config-influxdb-interval duration
(env METRICS_BACKEND_CONFIG_INFLUXDB_INTERVAL) (default 1m0s) (env METRICS_BACKEND_CONFIG_INFLUXDB_INTERVAL) (default 1m0s)
-metrics-backend-config-influxdb-password string -metrics-backend-config-influxdb-password string
(env METRICS_BACKEND_CONFIG_INFLUXDB_PASSWORD) (env METRICS_BACKEND_CONFIG_INFLUXDB_PASSWORD)
-metrics-backend-config-influxdb-retention-policy string -metrics-backend-config-influxdb-retention-policy string
(env METRICS_BACKEND_CONFIG_INFLUXDB_RETENTION_POLICY) (env METRICS_BACKEND_CONFIG_INFLUXDB_RETENTION_POLICY)
-metrics-backend-config-influxdb-tags value -metrics-backend-config-influxdb-tags value
any extra tags to be included with all reported metrics (env METRICS_BACKEND_CONFIG_INFLUXDB_TAGS) any extra tags to be included with all reported metrics (env METRICS_BACKEND_CONFIG_INFLUXDB_TAGS)
-metrics-backend-config-influxdb-username string -metrics-backend-config-influxdb-username string
(env METRICS_BACKEND_CONFIG_INFLUXDB_USERNAME) (env METRICS_BACKEND_CONFIG_INFLUXDB_USERNAME)
-ngrok-token string
If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable. (env NGROK_TOKEN)
-port port -port port
The port bound to listen for Minecraft client connections (env PORT) (default 25565) The port bound to listen for Minecraft client connections (env PORT) (default 25565)
-receive-proxy-protocol
Receive PROXY protocol from backend servers, by default trusts every proxy header that it receives, combine with -trusted-proxies to specify a list of trusted proxies (env RECEIVE_PROXY_PROTOCOL)
-routes-config string
Name or full path to routes config file (env ROUTES_CONFIG)
-simplify-srv
Simplify fully qualified SRV records for mapping (env SIMPLIFY_SRV)
-trusted-proxies value
Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol (env TRUSTED_PROXIES)
-use-proxy-protocol
Send PROXY protocol to backend servers (env USE_PROXY_PROTOCOL)
-version -version
Output version and exit (env VERSION) Output version and exit (env VERSION)
``` ```
# REST API
* `GET /routes` (with `Accept: application/json`) ## Docker Multi-Architecture Image
Retrieves the currently configured routes
* `POST /routes` (with `Content-Type: application/json`)
Registers a route given a JSON body structured like:
```json
{
"serverAddress": "CLIENT REQUESTED SERVER ADDRESS",
"backend": "HOST:PORT"
}
```
* `POST /defaultRoute` (with `Content-Type: application/json`)
Registers a default route to the given backend. JSON body is structured as:
```json
{
"backend": "HOST:PORT"
}
```
* `DELETE /routes/{serverAddress}`
Deletes an existing route for the given `serverAddress`
# Docker Multi-Architecture Image
The [multi-architecture image published at Docker Hub](https://hub.docker.com/repository/docker/itzg/mc-router) supports amd64, arm64, and arm32v6 (i.e. RaspberryPi). The [multi-architecture image published at Docker Hub](https://hub.docker.com/repository/docker/itzg/mc-router) supports amd64, arm64, and arm32v6 (i.e. RaspberryPi).
# Docker Compose Usage ## Docker Compose Usage
The following diagram shows how [the example docker-compose.yml](docs/docker-compose.yml) The diagram below shows how this `docker-compose.yml` configures two Minecraft server services named `vanilla` and `forge`, which also become the internal network aliases. _Notice those services don't need their ports exposed since the internal networking allows for the inter-container access._
configures two Minecraft server services named `vanilla` and `forge`, which also become the internal
network aliases. _Notice those services don't need their ports exposed since the internal
networking allows for the inter-container access._
The `router` service is only one of the services that needs to exposed on the external ```yaml
network. The `--mapping` declares how the hostname users will enter into their Minecraft client version: "3.8"
will map to the internal services.
services:
vanilla:
image: itzg/minecraft-server
environment:
EULA: "TRUE"
forge:
image: itzg/minecraft-server
environment:
EULA: "TRUE"
TYPE: FORGE
router:
image: ${MC_ROUTER_IMAGE:-itzg/mc-router}
depends_on:
- forge
- vanilla
environment:
MAPPING: |
vanilla.example.com=vanilla:25565
forge.example.com=forge:25565
ports:
- "25565:25565"
```
The `router` service is only one of the services that needs to exposed on the external network. The `MAPPING` declares how the hostname users will enter into their Minecraft client will map to the internal services.
![](docs/compose-diagram.png) ![](docs/compose-diagram.png)
To test out this example, I added these two entries to my "hosts" file: To test out this example, add these two entries to my "hosts" file:
``` ```
127.0.0.1 vanilla.example.com 127.0.0.1 vanilla.example.com
127.0.0.1 forge.example.com 127.0.0.1 forge.example.com
``` ```
# Kubernetes Usage ### Using Docker auto-discovery
## Using Kubernetes Service auto-discovery When running `mc-router` in a Docker environment you can pass the `--in-docker` or `--in-docker-swarm`
command-line argument and it will poll the Docker API periodically to find all the running
containers/services for Minecraft instances. To enable discovery you have to set the `mc-router.host`
label on the container. These are the labels scanned:
When running `mc-router` as a Kubernetes Pod and you pass the `--in-kube-cluster` command-line argument, then - `mc-router.host`: Used to configure the hostname the Minecraft clients would use to
it will automatically watch for any services annotated with connect to the server. The container/service endpoint will be used as the routed backend. You can
- `mc-router.itzg.me/externalServerName` : The value of the annotation will be registered as the external hostname Minecraft clients would used to connect to the use more than one hostname by splitting it with a comma.
routed service. The service's clusterIP and target port are used as the routed backend. You can use more hostnames by splitting them with comma. - `mc-router.port`: This value must be set to the port the Minecraft server is listening on.
- `mc-router.itzg.me/defaultServer` : The service's clusterIP and target port are used as the default if The default value is 25565.
no other `externalServiceName` annotations applies. - `mc-router.default`: Set this to a truthy value to make this server the default backend.
Please note that `mc-router.host` is still required to be set.
- `mc-router.network`: Specify the network you are using for the router if multiple are
present in the container/service. You can either use the network ID, it's full name or an alias.
#### Example Docker deployment
Refer to [this example docker-compose.yml](docs/sd-docker.docker-compose.yml) to see how to
configure two different Minecraft servers and a `mc-router` instance for use with Docker service discovery.
#### Example Docker Swarm deployment
Refer to [this example docker-compose.yml](docs/swarm.docker-compose.yml) to see how to
configure two different Minecraft servers and a `mc-router` instance for use with Docker Swarm service discovery.
## Routing Configuration
The routing configuration allows routing via a config file rather than a command.
You need to set `-routes-config` or `ROUTES_CONFIG` env variable.
The following shows a JSON file for routes config, where `default-server` can also be `null` or omitted:
```json
{
"default-server": "vanilla:25565",
"mappings": {
"vanilla.example.com": "vanilla:25565",
"forge.example.com": "forge:25565"
}
}
```
## Kubernetes Usage
### Using Kubernetes Service auto-discovery
When running `mc-router` as a Kubernetes Pod and you pass the `--in-kube-cluster` command-line argument, then it will automatically watch for any services annotated with
- `mc-router.itzg.me/externalServerName` : The value of the annotation will be registered as the external hostname Minecraft clients would used to connect to the routed service. The service's clusterIP and target port are used as the routed backend. You can use more hostnames by splitting them with comma.
- `mc-router.itzg.me/defaultServer` : The service's clusterIP and target port are used as the default if no other `externalServiceName` annotations applies.
For example, start `mc-router`'s container spec with For example, start `mc-router`'s container spec with
@ -148,7 +199,9 @@ metadata:
"mc-router.itzg.me/externalServerName": "external.host.name,other.host.name" "mc-router.itzg.me/externalServerName": "external.host.name,other.host.name"
``` ```
## Example Kubernetes deployment mc-router will pick the service port named either `minecraft` or `mc-router`. If neither port names exist, it will use port value 25565.
### Example Kubernetes deployment
[This example deployment](docs/k8s-example-auto.yaml) [This example deployment](docs/k8s-example-auto.yaml)
* Declares an `mc-router` service that exposes a node port 25565 * Declares an `mc-router` service that exposes a node port 25565
@ -163,12 +216,12 @@ kubectl apply -f https://raw.githubusercontent.com/itzg/mc-router/master/docs/k8
![](docs/example-deployment-auto.drawio.png) ![](docs/example-deployment-auto.drawio.png)
#### Notes ##### Notes
* This deployment assumes two persistent volume claims: `mc-stable` and `mc-snapshot` * This deployment assumes two persistent volume claims: `mc-stable` and `mc-snapshot`
* I extended the allowed node port range by adding `--service-node-port-range=25000-32767` * I extended the allowed node port range by adding `--service-node-port-range=25000-32767`
to `/etc/kubernetes/manifests/kube-apiserver.yaml` to `/etc/kubernetes/manifests/kube-apiserver.yaml`
#### Auto Scale Up ##### Auto Scale Up
The `-auto-scale-up` flag argument makes the router "wake up" any stopped backend servers, by changing `replicas: 0` to `replicas: 1`. The `-auto-scale-up` flag argument makes the router "wake up" any stopped backend servers, by changing `replicas: 0` to `replicas: 1`.
@ -191,39 +244,120 @@ rules:
verbs: ["watch","list","get","update"] verbs: ["watch","list","get","update"]
``` ```
# Docker Swarm Usage Make sure to set `StatefulSet.metadata.name` and `StatefulSet.spec.serviceName` to the same value;
otherwise, autoscaling will not trigger:
## Using Docker Swarm Service auto-discovery ```yaml
apiVersion: v1
kind: Service
metadata:
name: mc-forge
spec:
type: ClusterIP
annotations:
"mc-router.itzg.me/defaultServer": "true"
"mc-router.itzg.me/externalServerName": "external.host.name"
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mc-forge
spec:
serviceName: mc-forge
```
When running `mc-router` in a Docker Swarm environment you can pass the `--in-docker-swarm` ## REST API
command-line argument and it will poll the Docker API periodically to find all the running
services for minecraft instances. To enable discovery you have to set the `mc-router.host`
label on the service. These are the labels scanned:
- `mc-router.host`: Used to configure the hostname the Minecraft clients would use to * `GET /routes` (with `Accept: application/json`)
connect to the server. The service endpoint will be used as the routed backend. You can
use more than one hostname by splitting it with a comma.
- `mc-router.port`: This value must be set to the port the Minecraft server is listening on.
The default value is 25565.
- `mc-router.default`: Set this to a truthy value to make this server the deafult backend.
Please note that `mc-router.host` is still required to be set.
## Example Docker Swarm deployment Retrieves the currently configured routes
Refer to [this example docker-compose.yml](docs/swarm.docker-compose.yml) to see how to * `POST /routes` (with `Content-Type: application/json`)
configure two different Minecraft servers and a `mc-router` instance. Notice how you don't
have to expose the Minecraft instances ports, but all the containers are required to be in
the same network.
# Development Registers a route given a JSON body structured like:
```json
{
"serverAddress": "CLIENT REQUESTED SERVER ADDRESS",
"backend": "HOST:PORT"
}
```
## Building locally with Docker * `POST /defaultRoute` (with `Content-Type: application/json`)
Registers a default route to the given backend. JSON body is structured as:
```json
{
"backend": "HOST:PORT"
}
```
* `DELETE /routes/{serverAddress}`
Deletes an existing route for the given `serverAddress`
## ngrok
mc-router has built-in support to run as an [ngrok agent](https://ngrok.com/docs/secure-tunnels/ngrok-agent/). To enable this support, pass [an ngrok authtoken](https://ngrok.com/docs/secure-tunnels/ngrok-agent/tunnel-authtokens/#per-agent-authtokens) to the command-line argument or environment variable, [shown above](#usage).
### Ngrok Quick Start
Create/access an ngrok account and [allocate an agent authtoken from the dashboard](https://dashboard.ngrok.com/tunnels/authtokens).
In a new directory, create a file called `.env` with the allocated token
```dotenv
NGROK_TOKEN=...
```
In the same directory, create the following compose file:
```yaml
version: "3.8"
services:
mc:
image: itzg/minecraft-server
environment:
EULA: true
volumes:
- mc-data:/data
# No port mapping since mc-router connects over compose network
router:
image: itzg/mc-router
environment:
DEFAULT: mc:25565
NGROK_TOKEN: ${NGROK_TOKEN}
# No port mapping needed since it routes through ngrok tunnel
volumes:
mc-data: {}
```
Start the compose project:
```shell
docker compose up -d
```
Grab the mc-router logs using:
```shell
docker compose logs router
```
From those logs, locate the `ngrokUrl` parameter from the "Listening" info log message, such as `tcp://8.tcp.ngrok.io:99999`.
In the Minecraft client, the server address will be the part after the "tcp://" prefix, such as `8.tcp.ngrok.io:99999`.
## Development
### Building locally with Docker
```bash ```bash
docker build -t mc-router . docker build -t mc-router .
``` ```
## Build locally without Docker ### Build locally without Docker
After [installing Go](https://go.dev/doc/install) and doing a `go mod download` to install all required prerequisites, just like the [Dockerfile](Dockerfile) does, you can: After [installing Go](https://go.dev/doc/install) and doing a `go mod download` to install all required prerequisites, just like the [Dockerfile](Dockerfile) does, you can:
@ -232,7 +366,7 @@ make test # go test -v ./...
go build ./cmd/mc-router/ go build ./cmd/mc-router/
``` ```
## Skaffold ### Skaffold
For "in-cluster development" it's convenient to use https://skaffold.dev. Any changes to Go source code For "in-cluster development" it's convenient to use https://skaffold.dev. Any changes to Go source code
will trigger a go build, new container image pushed to registry with a new tag, and refresh in Kubernetes will trigger a go build, new container image pushed to registry with a new tag, and refresh in Kubernetes
@ -246,7 +380,7 @@ then add the _Artifact Registry Reader_ Role to the _Compute Engine default serv
then use e.g. `gcloud auth configure-docker europe-docker.pkg.dev` or equivalent one time (to create a `~/.docker/config.json`), then use e.g. `gcloud auth configure-docker europe-docker.pkg.dev` or equivalent one time (to create a `~/.docker/config.json`),
and then use e.g. `--default-repo=europe-docker.pkg.dev/YOUR-PROJECT/YOUR-ARTIFACT-REGISTRY` option for `skaffold dev`. and then use e.g. `--default-repo=europe-docker.pkg.dev/YOUR-PROJECT/YOUR-ARTIFACT-REGISTRY` option for `skaffold dev`.
## Performing snapshot release with Docker ### Performing snapshot release with Docker
```bash ```bash
docker run -it --rm \ docker run -it --rm \
@ -256,6 +390,6 @@ docker run -it --rm \
release --snapshot --rm-dist release --snapshot --rm-dist
``` ```
# Related Projects ## Related Projects
* https://github.com/haveachin/infrared * https://github.com/haveachin/infrared

View File

@ -8,7 +8,6 @@ import (
"os/signal" "os/signal"
"runtime/pprof" "runtime/pprof"
"strconv" "strconv"
"strings"
"syscall" "syscall"
"time" "time"
@ -30,22 +29,29 @@ type MetricsBackendConfig struct {
} }
type Config struct { type Config struct {
Port int `default:"25565" usage:"The [port] bound to listen for Minecraft client connections"` Port int `default:"25565" usage:"The [port] bound to listen for Minecraft client connections"`
Mapping []string `usage:"Comma-separated or repeated mappings of externalHostname=host:port"` Default string `usage:"host:port of a default Minecraft server to use when mapping not found"`
ApiBinding string `usage:"The [host:port] bound for servicing API requests"` Mapping map[string]string `usage:"Comma or newline delimited or repeated mappings of externalHostname=host:port"`
Version bool `usage:"Output version and exit"` ApiBinding string `usage:"The [host:port] bound for servicing API requests"`
CpuProfile string `usage:"Enables CPU profiling and writes to given path"` Version bool `usage:"Output version and exit"`
Debug bool `usage:"Enable debug logs"` CpuProfile string `usage:"Enables CPU profiling and writes to given path"`
ConnectionRateLimit int `default:"1" usage:"Max number of connections to allow per second"` Debug bool `usage:"Enable debug logs"`
InKubeCluster bool `usage:"Use in-cluster Kubernetes config"` ConnectionRateLimit int `default:"1" usage:"Max number of connections to allow per second"`
KubeConfig string `usage:"The path to a Kubernetes configuration file"` InKubeCluster bool `usage:"Use in-cluster Kubernetes config"`
AutoScaleUp bool `usage:"Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed"` KubeConfig string `usage:"The path to a Kubernetes configuration file"`
InDockerSwarm bool `usage:"Use in-swarm Docker config"` AutoScaleUp bool `usage:"Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed"`
DockerTimeout int `default:"0" usage:"Timeout configuration in seconds for the Docker Swarm integration"` InDocker bool `usage:"Use Docker service discovery"`
DockerRefreshInterval int `default:"15" usage:"Refresh interval in seconds for the Docker Swarm integration"` InDockerSwarm bool `usage:"Use Docker Swarm service discovery"`
MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb"` DockerSocket string `default:"unix:///var/run/docker.sock" usage:"Path to Docker socket to use"`
UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"` DockerTimeout int `default:"0" usage:"Timeout configuration in seconds for the Docker integrations"`
DockerRefreshInterval int `default:"15" usage:"Refresh interval in seconds for the Docker integrations"`
MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb"`
UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"`
ReceiveProxyProtocol bool `default:"false" usage:"Receive PROXY protocol from backend servers, by default trusts every proxy header that it receives, combine with -trusted-proxies to specify a list of trusted proxies"`
TrustedProxies []string `usage:"Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol"`
MetricsBackendConfig MetricsBackendConfig MetricsBackendConfig MetricsBackendConfig
RoutesConfig string `usage:"Name or full path to routes config file"`
NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."`
SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"` SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"`
} }
@ -100,12 +106,35 @@ func main() {
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
server.Routes.RegisterAll(parseMappings(config.Mapping)) if config.RoutesConfig != "" {
err := server.RoutesConfig.ReadRoutesConfig(config.RoutesConfig)
if err != nil {
logrus.WithError(err).Error("Unable to load routes from config file")
}
}
server.Routes.RegisterAll(config.Mapping)
if config.Default != "" {
server.Routes.SetDefaultRoute(config.Default)
}
if config.ConnectionRateLimit < 1 { if config.ConnectionRateLimit < 1 {
config.ConnectionRateLimit = 1 config.ConnectionRateLimit = 1
} }
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol)
trustedIpNets := make([]*net.IPNet, 0)
for _, ip := range config.TrustedProxies {
_, ipNet, err := net.ParseCIDR(ip)
if err != nil {
logrus.WithError(err).Fatal("Unable to parse trusted proxy CIDR block")
}
trustedIpNets = append(trustedIpNets, ipNet)
}
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets)
if config.NgrokToken != "" {
connector.UseNgrok(config.NgrokToken)
}
err = connector.StartAcceptingConnections(ctx, err = connector.StartAcceptingConnections(ctx,
net.JoinHostPort("", strconv.Itoa(config.Port)), net.JoinHostPort("", strconv.Itoa(config.Port)),
config.ConnectionRateLimit, config.ConnectionRateLimit,
@ -134,12 +163,21 @@ func main() {
} }
} }
if config.InDocker {
err = server.DockerWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval)
if err != nil {
logrus.WithError(err).Fatal("Unable to start docker integration")
} else {
defer server.DockerWatcher.Stop()
}
}
if config.InDockerSwarm { if config.InDockerSwarm {
err = server.DockerWatcher.StartInSwarm(config.DockerTimeout, config.DockerRefreshInterval) err = server.DockerSwarmWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval)
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Unable to start docker swarm integration") logrus.WithError(err).Fatal("Unable to start docker swarm integration")
} else { } else {
defer server.DockerWatcher.Stop() defer server.DockerSwarmWatcher.Stop()
} }
} }
@ -157,17 +195,3 @@ func main() {
connector.WaitForConnections() connector.WaitForConnections()
logrus.Info("Stopped") logrus.Info("Stopped")
} }
func parseMappings(vals []string) map[string]string {
result := make(map[string]string)
for _, part := range vals {
keyValue := strings.Split(part, "=")
if len(keyValue) == 2 {
result[keyValue[0]] = keyValue[1]
} else {
logrus.WithField("part", part).Fatal("Invalid part of mapping")
}
}
return result
}

1
docs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.env

View File

@ -1,5 +1,3 @@
version: '3.4'
services: services:
vanilla: vanilla:
image: itzg/minecraft-server image: itzg/minecraft-server
@ -18,9 +16,12 @@ services:
environment: environment:
# enable API # enable API
API_BINDING: ":25564" API_BINDING: ":25564"
DEBUG: true
MAPPING: |
vanilla.example.com=vanilla:25565
forge.example.com=forge:25565
ports: ports:
- 25565:25565 - "25565:25565"
# bind the API port to only loopback to avoid external exposure # bind the API port to only loopback to avoid external exposure
- 127.0.0.1:25564:25564 - "127.0.0.1:25564:25564"
command: --mapping=vanilla.example.com=vanilla:25565,forge.example.com=forge:25565

278
docs/k8s-autoscale.yaml Normal file
View File

@ -0,0 +1,278 @@
# This YAML is an example and is not intended to be directly applied.
# It consists of 3 parts
# 1. the mc-router with service account an service
# 2. the shutdown cronjob with service account
# 3. the actual server with service and storage
# part 3 is the only part you need to replicate for the number of servers you want
# mc-router
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: mc-router
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: services-watcher
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["watch","list"]
- apiGroups: ["apps"]
resources: ["statefulsets", "statefulsets/scale"]
verbs: ["watch","list","get","update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: mc-router-services-watcher
subjects:
- kind: ServiceAccount
name: mc-router
namespace: default
roleRef:
kind: ClusterRole
name: services-watcher
apiGroup: rbac.authorization.k8s.io
# Use whatever tcp ingress method you want I just used a node port here for simplicity
---
apiVersion: v1
kind: Service
metadata:
name: mc-router
spec:
type: NodePort
externalIPs:
- 192.168.1.100
ports:
- targetPort: web
name: web
port: 8080
nodePort: 30001
- targetPort: proxy
name: proxy
port: 25565
nodePort: 30000
selector:
run: mc-router
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
run: mc-router
name: mc-router
spec:
selector:
matchLabels:
run: mc-router
strategy:
type: Recreate
template:
metadata:
labels:
run: mc-router
spec:
serviceAccountName: mc-router
containers:
- image: aapjeisbaas/mc-router:latest
imagePullPolicy: Always
name: mc-router
args: ["--api-binding", ":8080", "--in-kube-cluster","--auto-scale-up", "--debug"]
env:
- name: AUTO_SCALE_UP
value: "true"
ports:
- name: proxy
containerPort: 25565
- name: web
containerPort: 8080
resources:
requests:
memory: 50Mi
cpu: "100m"
limits:
memory: 100Mi
cpu: "250m"
# Cron job for stopping empty servers
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: mc-shutdown
rules:
- apiGroups: ["apps"]
resources: ["statefulsets", "statefulsets/scale"]
verbs: ["list","get","update", "patch"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: mc-shutdown
subjects:
- kind: ServiceAccount
name: mc-shutdown
namespace: default
roleRef:
kind: ClusterRole
name: mc-shutdown
apiGroup: "rbac.authorization.k8s.io"
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: mc-shutdown
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: mc-shutdown
spec:
schedule: "*/5 * * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
serviceAccountName: mc-shutdown
restartPolicy: OnFailure
containers:
- name: shutdown
image: bitnami/kubectl:latest
imagePullPolicy: IfNotPresent
command:
- /bin/bash
- -c
- source shutdown-script.sh
volumeMounts:
- name: shutdown-script
mountPath: /shutdown-script.sh
subPath: shutdown-script.sh
readOnly: true
volumes:
- name: shutdown-script
configMap:
name: shutdown-script
items:
- key: shutdown-script.sh
path: shutdown-script.sh
# uses container label containertype=minecraft-server to find running servers
# TODO: get ownerReferences link to StatefulSet/name from pod metadate instead of sed string manipulation
---
apiVersion: v1
kind: ConfigMap
metadata:
name: shutdown-script
data:
shutdown-script.sh: |
#!/bin/bash
MC_PODS=$(kubectl get pods -l containertype=minecraft-server -o=jsonpath="{range .items[*]}{.metadata.name},"| sed 's/,/\n/g')
for p in $MC_PODS; do
echo "found minecraft pod $p, sleeping 120 seconds to prevent shutdown before login"
sleep 120
deployment=$(echo $p |sed 's/-0//g')
# check online player count in the mc server
if [[ $(kubectl exec -i $p -- /usr/local/bin/mc-monitor status) == *"online=0"* ]] ;then
kubectl scale statefulset $deployment --replicas=0
fi
done
# The actual minecraft servers, services and storage, repeat this block for as many servers as you want
# make sure you have the label containertype=minecraft-server this is used to find running servers
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: minecraft-servername-deployment
labels:
app: minecraft-servername-container
spec:
serviceName: minecraft-servername-deployment
selector:
matchLabels:
app: minecraft-servername-container
replicas: 0
template:
metadata:
labels:
app: minecraft-servername-container
containertype: minecraft-server
spec:
containers:
- name: minecraft-servername-deployment
image: itzg/minecraft-server:latest
imagePullPolicy: IfNotPresent
resources:
limits:
memory: "2048Mi"
env:
# Use secret in real usage
- name: EULA
value: "true"
# let the JVM figure out mem management
- name: "MEMORY"
value: ""
- name: JVM_XX_OPTS
value: "-XX:MaxRAMPercentage=75"
ports:
- containerPort: 25565
name: main
readinessProbe:
exec:
command: [ "/usr/local/bin/mc-monitor", "status", "--host", "localhost" ]
# Give it i + p * f seconds to be ready, so 120 seconds
initialDelaySeconds: 20
periodSeconds: 5
failureThreshold: 20
# Monitor ongoing liveness
livenessProbe:
exec:
command: ["/usr/local/bin/mc-monitor", "status", "--host", "localhost"]
initialDelaySeconds: 120
periodSeconds: 60
volumeMounts:
- name: mc-data
mountPath: /data
volumes:
- name: mc-data
persistentVolumeClaim:
claimName: minecraft-servername-pvc
readOnly: false
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: minecraft-servername-pvc
namespace: default
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 20Gi
---
apiVersion: v1
kind: Service
metadata:
name: minecraft-servername-deployment
namespace: default
annotations:
"mc-router.itzg.me/externalServerName": "your-awesome-server.public-domain.com"
spec:
ports:
- port: 25565
selector:
app: minecraft-servername-container

View File

@ -0,0 +1,19 @@
version: "3.8"
services:
mc:
image: itzg/minecraft-server
environment:
EULA: true
volumes:
- mc-data:/data
# No port mapping since mc-router connects over compose network
router:
image: itzg/mc-router
environment:
DEFAULT: mc:25565
NGROK_TOKEN: ${NGROK_TOKEN}
# No port mapping needed since it routes through ngrok tunnel
volumes:
mc-data: {}

View File

@ -0,0 +1,34 @@
services:
mcfoo:
image: itzg/minecraft-server
environment:
EULA: "TRUE"
MOTD: "foo"
volumes:
- mcfoodata:/data
labels:
mc-router.host: "foo.localhost"
mc-router.default: true
mcbar:
image: itzg/minecraft-server
environment:
EULA: "TRUE"
MOTD: "bar"
volumes:
- mcbardata:/data
labels:
mc-router.host: "bar.localhost"
router:
image: itzg/mc-router:latest
command: "-debug -in-docker"
depends_on:
- mcfoo
- mcbar
ports:
- "25565:25565"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
volumes:
mcfoodata:
mcbardata:

View File

@ -23,6 +23,7 @@ services:
replicas: 1 replicas: 1
labels: labels:
- "mc-router.host=foo.host.name,bar.host.name" - "mc-router.host=foo.host.name,bar.host.name"
- "mc-router.network=minecraft" # not required in this case
volumes: volumes:
- mcfoobardata:/data - mcfoobardata:/data
networks: networks:
@ -39,6 +40,9 @@ services:
replicas: 1 replicas: 1
labels: labels:
- "mc-router.host=baz.host.name" - "mc-router.host=baz.host.name"
- "mc-router.network=minecraft" # required since we are exposing a port
ports:
- "25575:25575" # RCON
volumes: volumes:
- mcbazdata:/data - mcbazdata:/data
networks: networks:

102
go.mod
View File

@ -1,68 +1,98 @@
module github.com/itzg/mc-router module github.com/itzg/mc-router
go 1.17 go 1.21
toolchain go1.22.5
require ( require (
github.com/go-kit/kit v0.12.0 github.com/go-kit/kit v0.13.0
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.1
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
github.com/itzg/go-flagsfiller v1.9.1 github.com/itzg/go-flagsfiller v1.14.0
github.com/juju/ratelimit v1.0.2 github.com/juju/ratelimit v1.0.2
github.com/pires/go-proxyproto v0.6.2 github.com/pires/go-proxyproto v0.7.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.9.0
golang.org/x/text v0.4.0 golang.ngrok.com/ngrok v1.10.0
k8s.io/api v0.18.5 golang.org/x/text v0.18.0
k8s.io/apimachinery v0.18.5 k8s.io/api v0.28.3
k8s.io/client-go v0.18.5 k8s.io/apimachinery v0.28.3
k8s.io/client-go v0.28.3
)
require (
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible // indirect
github.com/inconshreveable/log15/v3 v3.0.0-testing.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/sdk v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.ngrok.com/muxado/v2 v2.0.0 // indirect
golang.org/x/sync v0.8.0 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
) )
require ( require (
github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/VividCortex/gohistogram v1.0.0 // indirect github.com/VividCortex/gohistogram v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/docker v27.3.1+incompatible
github.com/docker/docker v20.10.19+incompatible
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/go-kit/log v0.2.0 // indirect github.com/go-kit/log v0.2.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.6 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.1.2 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gnostic v0.2.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/iancoleman/strcase v0.2.0 // indirect
github.com/imdario/mergo v0.3.7 // indirect github.com/imdario/mergo v0.3.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 // indirect golang.org/x/net v0.23.0 // indirect
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect golang.org/x/sys v0.18.0 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/term v0.18.0 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect golang.org/x/time v0.3.0 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/appengine v1.6.6 // indirect google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.3.0 // indirect gotest.tools/v3 v3.3.0 // indirect
k8s.io/klog v1.0.0 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 // indirect sigs.k8s.io/yaml v1.3.0 // indirect
sigs.k8s.io/structured-merge-diff/v3 v3.0.0 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
) )

937
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -160,6 +160,12 @@ func ReadFrame(reader io.Reader, addr net.Addr) (*Frame, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Limit frame length to 2^21 - 1
if frame.Length > 2097151 {
return nil, errors.Errorf("frame length %d too large", frame.Length)
}
logrus. logrus.
WithField("client", addr). WithField("client", addr).
WithField("length", frame.Length). WithField("length", frame.Length).

View File

@ -5,11 +5,13 @@ import (
"context" "context"
"io" "io"
"net" "net"
"strconv"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"golang.ngrok.com/ngrok"
"golang.ngrok.com/ngrok/config"
"github.com/go-kit/kit/metrics" "github.com/go-kit/kit/metrics"
"github.com/itzg/mc-router/mcproto" "github.com/itzg/mc-router/mcproto"
"github.com/juju/ratelimit" "github.com/juju/ratelimit"
@ -30,38 +32,94 @@ type ConnectorMetrics struct {
ActiveConnections metrics.Gauge ActiveConnections metrics.Gauge
} }
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool) *Connector { func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet) *Connector {
return &Connector{ return &Connector{
metrics: metrics, metrics: metrics,
sendProxyProto: sendProxyProto, sendProxyProto: sendProxyProto,
connectionsCond: sync.NewCond(&sync.Mutex{}), connectionsCond: sync.NewCond(&sync.Mutex{}),
receiveProxyProto: receiveProxyProto,
trustedProxyNets: trustedProxyNets,
} }
} }
type Connector struct { type Connector struct {
state mcproto.State state mcproto.State
metrics *ConnectorMetrics metrics *ConnectorMetrics
sendProxyProto bool sendProxyProto bool
receiveProxyProto bool
trustedProxyNets []*net.IPNet
activeConnections int32 activeConnections int32
connectionsCond *sync.Cond connectionsCond *sync.Cond
ngrokToken string
} }
func (c *Connector) StartAcceptingConnections(ctx context.Context, listenAddress string, connRateLimit int) error { func (c *Connector) StartAcceptingConnections(ctx context.Context, listenAddress string, connRateLimit int) error {
ln, err := c.createListener(ctx, listenAddress)
ln, err := net.Listen("tcp", listenAddress)
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Unable to start listening")
return err return err
} }
logrus.WithField("listenAddress", listenAddress).Info("Listening for Minecraft client connections")
go c.acceptConnections(ctx, ln, connRateLimit) go c.acceptConnections(ctx, ln, connRateLimit)
return nil return nil
} }
func (c *Connector) createListener(ctx context.Context, listenAddress string) (net.Listener, error) {
if c.ngrokToken != "" {
ngrokTun, err := ngrok.Listen(ctx,
config.TCPEndpoint(),
ngrok.WithAuthtoken(c.ngrokToken),
)
if err != nil {
logrus.WithError(err).Fatal("Unable to start ngrok tunnel")
return nil, err
}
logrus.WithField("ngrokUrl", ngrokTun.URL()).Info("Listening for Minecraft client connections via ngrok tunnel")
return ngrokTun, nil
}
listener, err := net.Listen("tcp", listenAddress)
if err != nil {
logrus.WithError(err).Fatal("Unable to start listening")
return nil, err
}
logrus.WithField("listenAddress", listenAddress).Info("Listening for Minecraft client connections")
if c.receiveProxyProto {
proxyListener := &proxyproto.Listener{
Listener: listener,
Policy: c.createProxyProtoPolicy(),
}
logrus.Info("Using PROXY protocol listener")
return proxyListener, nil
}
return listener, nil
}
func (c *Connector) createProxyProtoPolicy() func(upstream net.Addr) (proxyproto.Policy, error) {
return func(upstream net.Addr) (proxyproto.Policy, error) {
trustedIpNets := c.trustedProxyNets
if len(trustedIpNets) == 0 {
logrus.Debug("No trusted proxy networks configured, using the PROXY header by default")
return proxyproto.USE, nil
}
upstreamIP := upstream.(*net.TCPAddr).IP
for _, ipNet := range trustedIpNets {
if ipNet.Contains(upstreamIP) {
logrus.WithField("upstream", upstream).Debug("IP is in trusted proxies, using the PROXY header")
return proxyproto.USE, nil
}
}
logrus.WithField("upstream", upstream).Debug("IP is not in trusted proxies, discarding PROXY header")
return proxyproto.IGNORE, nil
}
}
func (c *Connector) WaitForConnections() { func (c *Connector) WaitForConnections() {
c.connectionsCond.L.Lock() c.connectionsCond.L.Lock()
defer c.connectionsCond.L.Unlock() defer c.connectionsCond.L.Unlock()
@ -230,21 +288,29 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.
// PROXY protocol implementation // PROXY protocol implementation
if c.sendProxyProto { if c.sendProxyProto {
remoteHostStr, _, _ := net.SplitHostPort(backendHostPort)
sourceAddrStr, sourcePortStr, _ := net.SplitHostPort(clientAddr.String()) // Determine transport protocol for the PROXY header by "analyzing" the frontend connection's address
sourcePort, _ := strconv.Atoi(sourcePortStr) transportProtocol := proxyproto.TCPv4
ourHostIpPart, _, err := net.SplitHostPort(frontendConn.LocalAddr().String())
if err != nil {
logrus.
WithError(err).
WithField("localAddr", frontendConn.LocalAddr()).
Error("Failed to extract host part of our address")
_ = backendConn.Close()
return
}
ourFrontendIp := net.ParseIP(ourHostIpPart)
if ourFrontendIp.To4() == nil {
transportProtocol = proxyproto.TCPv6
}
header := &proxyproto.Header{ header := &proxyproto.Header{
Version: 2, Version: 2,
Command: proxyproto.PROXY, Command: proxyproto.PROXY,
TransportProtocol: proxyproto.TCPv4, TransportProtocol: transportProtocol,
SourceAddr: &net.TCPAddr{ SourceAddr: clientAddr,
IP: net.ParseIP(sourceAddrStr), DestinationAddr: frontendConn.LocalAddr(), // our end of the client's connection
Port: sourcePort,
},
DestinationAddr: &net.TCPAddr{
IP: net.ParseIP(remoteHostStr),
},
} }
_, err = header.WriteTo(backendConn) _, err = header.WriteTo(backendConn)
@ -322,3 +388,7 @@ func (c *Connector) pumpFrames(incoming io.Reader, outgoing io.Writer, errors ch
errors <- io.EOF errors <- io.EOF
} }
} }
func (c *Connector) UseNgrok(token string) {
c.ngrokToken = token
}

77
server/connector_test.go Normal file
View File

@ -0,0 +1,77 @@
package server
import (
"net"
"testing"
"github.com/pires/go-proxyproto"
"github.com/stretchr/testify/assert"
)
func TestTrustedProxyNetworkPolicy(t *testing.T) {
tests := []struct {
name string
trustedNets []string
upstreamIP string
expectedPolicy proxyproto.Policy
}{
{
name: "trusted IP",
trustedNets: []string{"10.0.0.0/8"},
upstreamIP: "10.0.0.1",
expectedPolicy: proxyproto.USE,
},
{
name: "untrusted IP",
trustedNets: []string{"10.0.0.0/8"},
upstreamIP: "192.168.1.1",
expectedPolicy: proxyproto.IGNORE,
},
{
name: "multiple trusted nets",
trustedNets: []string{"10.0.0.0/8", "172.16.0.0/12"},
upstreamIP: "172.16.0.1",
expectedPolicy: proxyproto.USE,
},
{
name: "no trusted nets",
trustedNets: []string{},
upstreamIP: "148.184.129.202",
expectedPolicy: proxyproto.USE,
},
{
name: "remote trusted IP",
trustedNets: []string{"203.0.113.0/24"},
upstreamIP: "203.0.113.10",
expectedPolicy: proxyproto.USE,
},
{
name: "remote untrusted IP",
trustedNets: []string{"203.0.113.0/24"},
upstreamIP: "198.51.100.1",
expectedPolicy: proxyproto.IGNORE,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := &Connector{
trustedProxyNets: parseTrustedProxyNets(test.trustedNets),
}
policy := c.createProxyProtoPolicy()
upstreamAddr := &net.TCPAddr{IP: net.ParseIP(test.upstreamIP)}
policyResult, _ := policy(upstreamAddr)
assert.Equal(t, test.expectedPolicy, policyResult, "Unexpected policy result for %s", test.name)
})
}
}
func parseTrustedProxyNets(nets []string) []*net.IPNet {
parsedNets := make([]*net.IPNet, 0, len(nets))
for _, n := range nets {
_, ipNet, _ := net.ParseCIDR(n)
parsedNets = append(parsedNets, ipNet)
}
return parsedNets
}

View File

@ -3,26 +3,30 @@ package server
import ( import (
"context" "context"
"fmt" "fmt"
"net"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
dockertypes "github.com/docker/docker/api/types" dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
swarmtypes "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type IDockerWatcher interface { type IDockerWatcher interface {
StartInSwarm(timeoutSeconds int, refreshIntervalSeconds int) error Start(socket string, timeoutSeconds int, refreshIntervalSeconds int) error
Stop() Stop()
} }
const (
DockerAPIVersion = "1.24"
DockerRouterLabelHost = "mc-router.host"
DockerRouterLabelPort = "mc-router.port"
DockerRouterLabelDefault = "mc-router.default"
DockerRouterLabelNetwork = "mc-router.network"
)
var DockerWatcher IDockerWatcher = &dockerWatcherImpl{} var DockerWatcher IDockerWatcher = &dockerWatcherImpl{}
type dockerWatcherImpl struct { type dockerWatcherImpl struct {
@ -31,28 +35,20 @@ type dockerWatcherImpl struct {
contextCancel context.CancelFunc contextCancel context.CancelFunc
} }
const ( func (w *dockerWatcherImpl) makeWakerFunc(_ *routableContainer) func(ctx context.Context) error {
DockerConfigHost = "unix:///var/run/docker.sock"
DockerAPIVersion = "1.24"
DockerRouterLabelHost = "mc-router.host"
DockerRouterLabelPort = "mc-router.port"
DockerRouterLabelDefault = "mc-router.default"
)
func (w *dockerWatcherImpl) makeWakerFunc(service *routableService) func(ctx context.Context) error {
return func(ctx context.Context) error { return func(ctx context.Context) error {
return nil return nil
} }
} }
func (w *dockerWatcherImpl) StartInSwarm(timeoutSeconds int, refreshIntervalSeconds int) error { func (w *dockerWatcherImpl) Start(socket string, timeoutSeconds int, refreshIntervalSeconds int) error {
var err error var err error
timeout := time.Duration(timeoutSeconds) * time.Second timeout := time.Duration(timeoutSeconds) * time.Second
refreshInterval := time.Duration(refreshIntervalSeconds) * time.Second refreshInterval := time.Duration(refreshIntervalSeconds) * time.Second
opts := []client.Opt{ opts := []client.Opt{
client.WithHost(DockerConfigHost), client.WithHost(socket),
client.WithTimeout(timeout), client.WithTimeout(timeout),
client.WithHTTPHeaders(map[string]string{ client.WithHTTPHeaders(map[string]string{
"User-Agent": "mc-router ", "User-Agent": "mc-router ",
@ -66,22 +62,22 @@ func (w *dockerWatcherImpl) StartInSwarm(timeoutSeconds int, refreshIntervalSeco
} }
ticker := time.NewTicker(refreshInterval) ticker := time.NewTicker(refreshInterval)
serviceMap := map[string]*routableService{} containerMap := map[string]*routableContainer{}
var ctx context.Context var ctx context.Context
ctx, w.contextCancel = context.WithCancel(context.Background()) ctx, w.contextCancel = context.WithCancel(context.Background())
initialServices, err := w.listServices(ctx) initialContainers, err := w.listContainers(ctx)
if err != nil { if err != nil {
return err return err
} }
for _, s := range initialServices { for _, c := range initialContainers {
serviceMap[s.externalServiceName] = s containerMap[c.externalContainerName] = c
if s.externalServiceName != "" { if c.externalContainerName != "" {
Routes.CreateMapping(s.externalServiceName, s.containerEndpoint, w.makeWakerFunc(s)) Routes.CreateMapping(c.externalContainerName, c.containerEndpoint, w.makeWakerFunc(c))
} else { } else {
Routes.SetDefaultRoute(s.containerEndpoint) Routes.SetDefaultRoute(c.containerEndpoint)
} }
} }
@ -89,43 +85,43 @@ func (w *dockerWatcherImpl) StartInSwarm(timeoutSeconds int, refreshIntervalSeco
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
services, err := w.listServices(ctx) containers, err := w.listContainers(ctx)
if err != nil { if err != nil {
logrus.WithError(err).Error("Docker failed to list services") logrus.WithError(err).Error("Docker failed to list containers")
return return
} }
visited := map[string]struct{}{} visited := map[string]struct{}{}
for _, rs := range services { for _, rs := range containers {
if oldRs, ok := serviceMap[rs.externalServiceName]; !ok { if oldRs, ok := containerMap[rs.externalContainerName]; !ok {
serviceMap[rs.externalServiceName] = rs containerMap[rs.externalContainerName] = rs
logrus.WithField("routableService", rs).Debug("ADD") logrus.WithField("routableContainer", rs).Debug("ADD")
if rs.externalServiceName != "" { if rs.externalContainerName != "" {
Routes.CreateMapping(rs.externalServiceName, rs.containerEndpoint, w.makeWakerFunc(rs)) Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, w.makeWakerFunc(rs))
} else { } else {
Routes.SetDefaultRoute(rs.containerEndpoint) Routes.SetDefaultRoute(rs.containerEndpoint)
} }
} else if oldRs.containerEndpoint != rs.containerEndpoint { } else if oldRs.containerEndpoint != rs.containerEndpoint {
serviceMap[rs.externalServiceName] = rs containerMap[rs.externalContainerName] = rs
if rs.externalServiceName != "" { if rs.externalContainerName != "" {
Routes.DeleteMapping(rs.externalServiceName) Routes.DeleteMapping(rs.externalContainerName)
Routes.CreateMapping(rs.externalServiceName, rs.containerEndpoint, w.makeWakerFunc(rs)) Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, w.makeWakerFunc(rs))
} else { } else {
Routes.SetDefaultRoute(rs.containerEndpoint) Routes.SetDefaultRoute(rs.containerEndpoint)
} }
logrus.WithFields(logrus.Fields{"old": oldRs, "new": rs}).Debug("UPDATE") logrus.WithFields(logrus.Fields{"old": oldRs, "new": rs}).Debug("UPDATE")
} }
visited[rs.externalServiceName] = struct{}{} visited[rs.externalContainerName] = struct{}{}
} }
for _, rs := range serviceMap { for _, rs := range containerMap {
if _, ok := visited[rs.externalServiceName]; !ok { if _, ok := visited[rs.externalContainerName]; !ok {
delete(serviceMap, rs.externalServiceName) delete(containerMap, rs.externalContainerName)
if rs.externalServiceName != "" { if rs.externalContainerName != "" {
Routes.DeleteMapping(rs.externalServiceName) Routes.DeleteMapping(rs.externalContainerName)
} else { } else {
Routes.SetDefaultRoute("") Routes.SetDefaultRoute("")
} }
logrus.WithField("routableService", rs).Debug("DELETE") logrus.WithField("routableContainer", rs).Debug("DELETE")
} }
} }
@ -136,64 +132,33 @@ func (w *dockerWatcherImpl) StartInSwarm(timeoutSeconds int, refreshIntervalSeco
} }
}() }()
logrus.Info("Monitoring Docker for Minecraft services") logrus.Info("Monitoring Docker for Minecraft containers")
return nil return nil
} }
func (w *dockerWatcherImpl) listServices(ctx context.Context) ([]*routableService, error) { func (w *dockerWatcherImpl) listContainers(ctx context.Context) ([]*routableContainer, error) {
services, err := w.client.ServiceList(ctx, dockertypes.ServiceListOptions{}) containers, err := w.client.ContainerList(ctx, container.ListOptions{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
serverVersion, err := w.client.ServerVersion(ctx) var result []*routableContainer
if err != nil { for _, container := range containers {
return nil, err data, ok := w.parseContainerData(&container)
}
networkListArgs := filters.NewArgs()
// https://docs.docker.com/engine/api/v1.29/#tag/Network (Docker 17.06)
if versions.GreaterThanOrEqualTo(serverVersion.APIVersion, "1.29") {
networkListArgs.Add("scope", "swarm")
} else {
networkListArgs.Add("driver", "overlay")
}
networkList, err := w.client.NetworkList(ctx, dockertypes.NetworkListOptions{Filters: networkListArgs})
if err != nil {
return nil, err
}
networkMap := make(map[string]*dockertypes.NetworkResource)
for _, network := range networkList {
networkToAdd := network
networkMap[network.ID] = &networkToAdd
}
var result []*routableService
for _, service := range services {
if service.Spec.EndpointSpec.Mode != swarmtypes.ResolutionModeVIP {
continue
}
if len(service.Endpoint.VirtualIPs) == 0 {
continue
}
data, ok := w.parseServiceData(&service)
if !ok { if !ok {
continue continue
} }
for _, host := range data.hosts { for _, host := range data.hosts {
result = append(result, &routableService{ result = append(result, &routableContainer{
containerEndpoint: fmt.Sprintf("%s:%d", data.ip, data.port), containerEndpoint: fmt.Sprintf("%s:%d", data.ip, data.port),
externalServiceName: host, externalContainerName: host,
}) })
} }
if data.def != nil && *data.def { if data.def != nil && *data.def {
result = append(result, &routableService{ result = append(result, &routableContainer{
containerEndpoint: fmt.Sprintf("%s:%d", data.ip, data.port), containerEndpoint: fmt.Sprintf("%s:%d", data.ip, data.port),
externalServiceName: "", externalContainerName: "",
}) })
} }
} }
@ -201,47 +166,44 @@ func (w *dockerWatcherImpl) listServices(ctx context.Context) ([]*routableServic
return result, nil return result, nil
} }
type parsedDockerServiceData struct { type parsedDockerContainerData struct {
hosts []string hosts []string
port uint64 port uint64
def *bool def *bool
ip string network *string
ip string
} }
func (w *dockerWatcherImpl) parseServiceData(service *swarm.Service) (data parsedDockerServiceData, ok bool) { func (w *dockerWatcherImpl) parseContainerData(container *dockertypes.Container) (data parsedDockerContainerData, ok bool) {
ok = true for key, value := range container.Labels {
for key, value := range service.Spec.Labels {
if key == DockerRouterLabelHost { if key == DockerRouterLabelHost {
if data.hosts != nil { if data.hosts != nil {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}). logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring service with duplicate %s", DockerRouterLabelHost) Warnf("ignoring container with duplicate %s label", DockerRouterLabelHost)
ok = false
return return
} }
data.hosts = strings.Split(value, ",") data.hosts = strings.Split(value, ",")
} }
if key == DockerRouterLabelPort { if key == DockerRouterLabelPort {
if data.port != 0 { if data.port != 0 {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}). logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring service with duplicate %s", DockerRouterLabelPort) Warnf("ignoring container with duplicate %s label", DockerRouterLabelPort)
ok = false
return return
} }
var err error var err error
data.port, err = strconv.ParseUint(value, 10, 32) data.port, err = strconv.ParseUint(value, 10, 32)
if err != nil { if err != nil {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}). logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
WithError(err). WithError(err).
Warnf("ignoring service with invalid %s", DockerRouterLabelPort) Warnf("ignoring container with invalid %s label", DockerRouterLabelPort)
ok = false
return return
} }
} }
if key == DockerRouterLabelDefault { if key == DockerRouterLabelDefault {
if data.def != nil { if data.def != nil {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}). logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring service with duplicate %s", DockerRouterLabelDefault) Warnf("ignoring container with duplicate %s label", DockerRouterLabelDefault)
ok = false
return return
} }
data.def = new(bool) data.def = new(bool)
@ -249,18 +211,75 @@ func (w *dockerWatcherImpl) parseServiceData(service *swarm.Service) (data parse
lowerValue := strings.TrimSpace(strings.ToLower(value)) lowerValue := strings.TrimSpace(strings.ToLower(value))
*data.def = lowerValue != "" && lowerValue != "0" && lowerValue != "false" && lowerValue != "no" *data.def = lowerValue != "" && lowerValue != "0" && lowerValue != "false" && lowerValue != "no"
} }
if key == DockerRouterLabelNetwork {
if data.network != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring container with duplicate %s label", DockerRouterLabelNetwork)
return
}
data.network = new(string)
*data.network = value
}
} }
// probably not minecraft related
if len(data.hosts) == 0 { if len(data.hosts) == 0 {
ok = false
return return
} }
if len(container.NetworkSettings.Networks) == 0 {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring container, no networks found")
return
}
if data.port == 0 { if data.port == 0 {
data.port = 25565 data.port = 25565
} }
virtualIP := service.Endpoint.VirtualIPs[0] if data.network != nil {
ip, _, _ := net.ParseCIDR(virtualIP.Addr) // Loop through all the container's networks and attempt to find one whose Network ID, Name, or Aliases match the
data.ip = ip.String() // specified network
for name, endpoint := range container.NetworkSettings.Networks {
if name == endpoint.NetworkID {
data.ip = endpoint.IPAddress
}
if name == *data.network {
data.ip = endpoint.IPAddress
break
}
for _, alias := range endpoint.Aliases {
if alias == name {
data.ip = endpoint.IPAddress
break
}
}
}
} else {
// If there's no endpoint specified we can just assume the only one is the network we should use. One caveat is
// if there's more than one network on this container, we should require that the user specifies a network to avoid
// weird problems.
if len(container.NetworkSettings.Networks) > 1 {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring container, multiple networks found and none specified using label %s", DockerRouterLabelNetwork)
return
}
for _, endpoint := range container.NetworkSettings.Networks {
data.ip = endpoint.IPAddress
break
}
}
if data.ip == "" {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring container, unable to find accessible ip address")
return
}
ok = true
return return
} }
@ -270,3 +289,8 @@ func (w *dockerWatcherImpl) Stop() {
w.contextCancel() w.contextCancel()
} }
} }
type routableContainer struct {
externalContainerName string
containerEndpoint string
}

321
server/docker_swarm.go Normal file
View File

@ -0,0 +1,321 @@
package server
import (
"context"
"fmt"
"net"
"strconv"
"strings"
"sync"
"time"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
swarmtypes "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
var DockerSwarmWatcher IDockerWatcher = &dockerSwarmWatcherImpl{}
type dockerSwarmWatcherImpl struct {
sync.RWMutex
client *client.Client
contextCancel context.CancelFunc
}
func (w *dockerSwarmWatcherImpl) makeWakerFunc(_ *routableService) func(ctx context.Context) error {
return func(ctx context.Context) error {
return nil
}
}
func (w *dockerSwarmWatcherImpl) Start(socket string, timeoutSeconds int, refreshIntervalSeconds int) error {
var err error
timeout := time.Duration(timeoutSeconds) * time.Second
refreshInterval := time.Duration(refreshIntervalSeconds) * time.Second
opts := []client.Opt{
client.WithHost(socket),
client.WithTimeout(timeout),
client.WithHTTPHeaders(map[string]string{
"User-Agent": "mc-router ",
}),
client.WithVersion(DockerAPIVersion),
}
w.client, err = client.NewClientWithOpts(opts...)
if err != nil {
return err
}
ticker := time.NewTicker(refreshInterval)
serviceMap := map[string]*routableService{}
var ctx context.Context
ctx, w.contextCancel = context.WithCancel(context.Background())
initialServices, err := w.listServices(ctx)
if err != nil {
return err
}
for _, s := range initialServices {
serviceMap[s.externalServiceName] = s
if s.externalServiceName != "" {
Routes.CreateMapping(s.externalServiceName, s.containerEndpoint, w.makeWakerFunc(s))
} else {
Routes.SetDefaultRoute(s.containerEndpoint)
}
}
go func() {
for {
select {
case <-ticker.C:
services, err := w.listServices(ctx)
if err != nil {
logrus.WithError(err).Error("Docker failed to list services")
return
}
visited := map[string]struct{}{}
for _, rs := range services {
if oldRs, ok := serviceMap[rs.externalServiceName]; !ok {
serviceMap[rs.externalServiceName] = rs
logrus.WithField("routableService", rs).Debug("ADD")
if rs.externalServiceName != "" {
Routes.CreateMapping(rs.externalServiceName, rs.containerEndpoint, w.makeWakerFunc(rs))
} else {
Routes.SetDefaultRoute(rs.containerEndpoint)
}
} else if oldRs.containerEndpoint != rs.containerEndpoint {
serviceMap[rs.externalServiceName] = rs
if rs.externalServiceName != "" {
Routes.DeleteMapping(rs.externalServiceName)
Routes.CreateMapping(rs.externalServiceName, rs.containerEndpoint, w.makeWakerFunc(rs))
} else {
Routes.SetDefaultRoute(rs.containerEndpoint)
}
logrus.WithFields(logrus.Fields{"old": oldRs, "new": rs}).Debug("UPDATE")
}
visited[rs.externalServiceName] = struct{}{}
}
for _, rs := range serviceMap {
if _, ok := visited[rs.externalServiceName]; !ok {
delete(serviceMap, rs.externalServiceName)
if rs.externalServiceName != "" {
Routes.DeleteMapping(rs.externalServiceName)
} else {
Routes.SetDefaultRoute("")
}
logrus.WithField("routableService", rs).Debug("DELETE")
}
}
case <-ctx.Done():
ticker.Stop()
return
}
}
}()
logrus.Info("Monitoring Docker Swarm for Minecraft services")
return nil
}
func (w *dockerSwarmWatcherImpl) listServices(ctx context.Context) ([]*routableService, error) {
services, err := w.client.ServiceList(ctx, dockertypes.ServiceListOptions{})
if err != nil {
return nil, err
}
serverVersion, err := w.client.ServerVersion(ctx)
if err != nil {
return nil, err
}
networkListArgs := filters.NewArgs()
// https://docs.docker.com/engine/api/v1.29/#tag/Network (Docker 17.06)
if versions.GreaterThanOrEqualTo(serverVersion.APIVersion, "1.29") {
networkListArgs.Add("scope", "swarm")
} else {
networkListArgs.Add("driver", "overlay")
}
networkList, err := w.client.NetworkList(ctx, network.ListOptions{Filters: networkListArgs})
if err != nil {
return nil, err
}
networkMap := make(map[string]*network.Inspect)
for _, network := range networkList {
networkToAdd := network
networkMap[network.ID] = &networkToAdd
}
var result []*routableService
for _, service := range services {
if service.Spec.EndpointSpec.Mode != swarmtypes.ResolutionModeVIP {
continue
}
if len(service.Endpoint.VirtualIPs) == 0 {
continue
}
data, ok := w.parseServiceData(&service, networkMap)
if !ok {
continue
}
for _, host := range data.hosts {
result = append(result, &routableService{
containerEndpoint: fmt.Sprintf("%s:%d", data.ip, data.port),
externalServiceName: host,
})
}
if data.def != nil && *data.def {
result = append(result, &routableService{
containerEndpoint: fmt.Sprintf("%s:%d", data.ip, data.port),
externalServiceName: "",
})
}
}
return result, nil
}
func dockerCheckNetworkName(id string, name string, networkMap map[string]*network.Inspect, networkAliases map[string][]string) (bool, error) {
// we allow to specify the id instead
if id == name {
return true, nil
}
if network := networkMap[id]; network != nil {
if network.Name == name {
return true, nil
}
aliases := networkAliases[id]
for _, alias := range aliases {
if alias == name {
return true, nil
}
}
return false, nil
}
return false, fmt.Errorf("network not found %s", id)
}
type parsedDockerServiceData struct {
hosts []string
port uint64
def *bool
network *string
ip string
}
func (w *dockerSwarmWatcherImpl) parseServiceData(service *swarm.Service, networkMap map[string]*network.Inspect) (data parsedDockerServiceData, ok bool) {
networkAliases := map[string][]string{}
for _, network := range service.Spec.TaskTemplate.Networks {
networkAliases[network.Target] = network.Aliases
}
for key, value := range service.Spec.Labels {
if key == DockerRouterLabelHost {
if data.hosts != nil {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}).
Warnf("ignoring service with duplicate %s", DockerRouterLabelHost)
return
}
data.hosts = strings.Split(value, ",")
}
if key == DockerRouterLabelPort {
if data.port != 0 {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}).
Warnf("ignoring service with duplicate %s", DockerRouterLabelPort)
return
}
var err error
data.port, err = strconv.ParseUint(value, 10, 32)
if err != nil {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}).
WithError(err).
Warnf("ignoring service with invalid %s", DockerRouterLabelPort)
return
}
}
if key == DockerRouterLabelDefault {
if data.def != nil {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}).
Warnf("ignoring service with duplicate %s", DockerRouterLabelDefault)
return
}
data.def = new(bool)
lowerValue := strings.TrimSpace(strings.ToLower(value))
*data.def = lowerValue != "" && lowerValue != "0" && lowerValue != "false" && lowerValue != "no"
}
if key == DockerRouterLabelNetwork {
if data.network != nil {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}).
Warnf("ignoring service with duplicate %s", DockerRouterLabelNetwork)
return
}
data.network = new(string)
*data.network = value
}
}
// probably not minecraft related
if len(data.hosts) == 0 {
return
}
if len(service.Endpoint.VirtualIPs) == 0 {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}).
Warnf("ignoring service, no VirtualIPs found")
return
}
if data.port == 0 {
data.port = 25565
}
vipIndex := -1
if data.network != nil {
for i, vip := range service.Endpoint.VirtualIPs {
if ok, err := dockerCheckNetworkName(vip.NetworkID, *data.network, networkMap, networkAliases); ok {
vipIndex = i
break
} else if err != nil {
// we intentionally ignore name check errors
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}).
Debugf("%v", err)
}
}
if vipIndex == -1 {
logrus.WithFields(logrus.Fields{"serviceId": service.ID, "serviceName": service.Spec.Name}).
Warnf("ignoring service, network %s not found", *data.network)
return
}
} else {
// if network isn't specified assume it's the first one
vipIndex = 0
}
virtualIP := service.Endpoint.VirtualIPs[vipIndex]
ip, _, _ := net.ParseCIDR(virtualIP.Addr)
data.ip = ip.String()
ok = true
return
}
func (w *dockerSwarmWatcherImpl) Stop() {
if w.contextCancel != nil {
w.contextCancel()
}
}

View File

@ -232,7 +232,7 @@ func (w *k8sWatcherImpl) buildDetails(service *core.Service, externalServiceName
clusterIp := service.Spec.ClusterIP clusterIp := service.Spec.ClusterIP
port := "25565" port := "25565"
for _, p := range service.Spec.Ports { for _, p := range service.Spec.Ports {
if p.Name == "mc-router" { if p.Name == "mc-router" || p.Name == "minecraft" {
port = strconv.Itoa(int(p.Port)) port = strconv.Itoa(int(p.Port))
} }
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"net/http" "net/http"
"regexp"
"strings" "strings"
"sync" "sync"
@ -11,6 +12,8 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var tcpShieldPattern = regexp.MustCompile("///.*")
func init() { func init() {
apiRoutes.Path("/routes").Methods("GET"). apiRoutes.Path("/routes").Methods("GET").
Headers("Accept", "application/json"). Headers("Accept", "application/json").
@ -24,7 +27,7 @@ func init() {
apiRoutes.Path("/routes/{serverAddress}").Methods("DELETE").HandlerFunc(routesDeleteHandler) apiRoutes.Path("/routes/{serverAddress}").Methods("DELETE").HandlerFunc(routesDeleteHandler)
} }
func routesListHandler(writer http.ResponseWriter, request *http.Request) { func routesListHandler(writer http.ResponseWriter, _ *http.Request) {
mappings := Routes.GetMappings() mappings := Routes.GetMappings()
bytes, err := json.Marshal(mappings) bytes, err := json.Marshal(mappings)
if err != nil { if err != nil {
@ -32,11 +35,15 @@ func routesListHandler(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusInternalServerError) writer.WriteHeader(http.StatusInternalServerError)
return return
} }
writer.Write(bytes) _, err = writer.Write(bytes)
if err != nil {
logrus.WithError(err).Error("Failed to write response")
}
} }
func routesDeleteHandler(writer http.ResponseWriter, request *http.Request) { func routesDeleteHandler(writer http.ResponseWriter, request *http.Request) {
serverAddress := mux.Vars(request)["serverAddress"] serverAddress := mux.Vars(request)["serverAddress"]
RoutesConfig.DeleteMapping(serverAddress)
if serverAddress != "" { if serverAddress != "" {
if Routes.DeleteMapping(serverAddress) { if Routes.DeleteMapping(serverAddress) {
writer.WriteHeader(http.StatusOK) writer.WriteHeader(http.StatusOK)
@ -52,6 +59,7 @@ func routesCreateHandler(writer http.ResponseWriter, request *http.Request) {
Backend string Backend string
}{} }{}
//goland:noinspection GoUnhandledErrorResult
defer request.Body.Close() defer request.Body.Close()
decoder := json.NewDecoder(request.Body) decoder := json.NewDecoder(request.Body)
@ -63,6 +71,7 @@ func routesCreateHandler(writer http.ResponseWriter, request *http.Request) {
} }
Routes.CreateMapping(definition.ServerAddress, definition.Backend, func(ctx context.Context) error { return nil }) Routes.CreateMapping(definition.ServerAddress, definition.Backend, func(ctx context.Context) error { return nil })
RoutesConfig.AddMapping(definition.ServerAddress, definition.Backend)
writer.WriteHeader(http.StatusCreated) writer.WriteHeader(http.StatusCreated)
} }
@ -71,6 +80,7 @@ func routesSetDefault(writer http.ResponseWriter, request *http.Request) {
Backend string Backend string
}{} }{}
//goland:noinspection GoUnhandledErrorResult
defer request.Body.Close() defer request.Body.Close()
decoder := json.NewDecoder(request.Body) decoder := json.NewDecoder(request.Body)
@ -82,6 +92,7 @@ func routesSetDefault(writer http.ResponseWriter, request *http.Request) {
} }
Routes.SetDefaultRoute(body.Backend) Routes.SetDefaultRoute(body.Backend)
RoutesConfig.SetDefaultRoute(body.Backend)
writer.WriteHeader(http.StatusOK) writer.WriteHeader(http.StatusOK)
} }
@ -143,16 +154,22 @@ func (r *routesImpl) SimplifySRV(srvEnabled bool) {
r.simplifySRV = srvEnabled r.simplifySRV = srvEnabled
} }
func (r *routesImpl) FindBackendForServerAddress(ctx context.Context, serverAddress string) (string, string, func(ctx context.Context) error) { func (r *routesImpl) FindBackendForServerAddress(_ context.Context, serverAddress string) (string, string, func(ctx context.Context) error) {
r.RLock() r.RLock()
defer r.RUnlock() defer r.RUnlock()
// Trim off Forge null-delimited address parts like \x00FML3\x00
serverAddress = strings.Split(serverAddress, "\x00")[0]
serverAddress = strings.ToLower(
// trim the root zone indicator, see https://en.wikipedia.org/wiki/Fully_qualified_domain_name
strings.TrimSuffix(serverAddress, "."))
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"serverAddress": serverAddress, "serverAddress": serverAddress,
}).Debug("Finding backend for server address") }).Debug("Finding backend for server address")
if r.simplifySRV { if r.simplifySRV {
serverAddress = strings.TrimSuffix(serverAddress, ".")
parts := strings.Split(serverAddress, ".") parts := strings.Split(serverAddress, ".")
tcpIndex := -1 tcpIndex := -1
for i, part := range parts { for i, part := range parts {
@ -168,18 +185,15 @@ func (r *routesImpl) FindBackendForServerAddress(ctx context.Context, serverAddr
serverAddress = strings.Join(parts, ".") serverAddress = strings.Join(parts, ".")
} }
addressParts := strings.Split(serverAddress, "\x00") // Strip suffix of TCP Shield
serverAddress = tcpShieldPattern.ReplaceAllString(serverAddress, "")
address := strings.ToLower(
// trim the root zone indicator, see https://en.wikipedia.org/wiki/Fully_qualified_domain_name
strings.TrimSuffix(addressParts[0], "."))
if r.mappings != nil { if r.mappings != nil {
if mapping, exists := r.mappings[address]; exists { if mapping, exists := r.mappings[serverAddress]; exists {
return mapping.backend, address, mapping.waker return mapping.backend, serverAddress, mapping.waker
} }
} }
return r.defaultRoute, address, nil return r.defaultRoute, serverAddress, nil
} }
func (r *routesImpl) GetMappings() map[string]string { func (r *routesImpl) GetMappings() map[string]string {

173
server/routes_config.go Normal file
View File

@ -0,0 +1,173 @@
package server
import (
"encoding/json"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"io/fs"
"os"
"sync"
)
type IRoutesConfig interface {
ReadRoutesConfig(routesConfig string)
AddMapping(serverAddress string, backend string)
DeleteMapping(serverAddress string)
SetDefaultRoute(backend string)
}
var RoutesConfig = &routesConfigImpl{}
type routesConfigImpl struct {
sync.RWMutex
fileName string
}
type routesConfigStructure struct {
DefaultServer string `json:"default-server"`
Mappings map[string]string `json:"mappings"`
}
func (r *routesConfigImpl) ReadRoutesConfig(routesConfig string) error {
r.fileName = routesConfig
logrus.WithField("routesConfig", r.fileName).Info("Loading routes config file")
config, readErr := r.readRoutesConfigFile()
if readErr != nil {
if errors.Is(readErr, fs.ErrNotExist) {
logrus.WithField("routesConfig", r.fileName).Info("Routes config file doses not exist, skipping reading it")
// File doesn't exist -> ignore it
return nil
}
return errors.Wrap(readErr, "Could not load the routes config file")
}
Routes.RegisterAll(config.Mappings)
Routes.SetDefaultRoute(config.DefaultServer)
return nil
}
func (r *routesConfigImpl) AddMapping(serverAddress string, backend string) {
if !r.isRoutesConfigEnabled() {
return
}
config, readErr := r.readRoutesConfigFile()
if readErr != nil && !errors.Is(readErr, fs.ErrNotExist) {
logrus.WithError(readErr).Error("Could not read the routes config file")
return
}
if config.Mappings == nil {
config.Mappings = make(map[string]string)
}
config.Mappings[serverAddress] = backend
writeErr := r.writeRoutesConfigFile(config)
if writeErr != nil {
logrus.WithError(writeErr).Error("Could not write to the routes config file")
return
}
logrus.WithFields(logrus.Fields{
"serverAddress": serverAddress,
"backend": backend,
}).Info("Added route to routes config")
return
}
func (r *routesConfigImpl) SetDefaultRoute(backend string) {
if !r.isRoutesConfigEnabled() {
return
}
config, readErr := r.readRoutesConfigFile()
if readErr != nil && !errors.Is(readErr, fs.ErrNotExist) {
logrus.WithError(readErr).Error("Could not read the routes config file")
return
}
config.DefaultServer = backend
writeErr := r.writeRoutesConfigFile(config)
if writeErr != nil {
logrus.WithError(writeErr).Error("Could not write to the routes config file")
return
}
logrus.WithFields(logrus.Fields{
"backend": backend,
}).Info("Set default route in routes config")
return
}
func (r *routesConfigImpl) DeleteMapping(serverAddress string) {
if !r.isRoutesConfigEnabled() {
return
}
config, readErr := r.readRoutesConfigFile()
if readErr != nil && !errors.Is(readErr, fs.ErrNotExist) {
logrus.WithError(readErr).Error("Could not read the routes config file")
return
}
delete(config.Mappings, serverAddress)
writeErr := r.writeRoutesConfigFile(config)
if writeErr != nil {
logrus.WithError(writeErr).Error("Could not write to the routes config file")
return
}
logrus.WithField("serverAddress", serverAddress).Info("Deleted route in routes config")
return
}
func (r *routesConfigImpl) isRoutesConfigEnabled() bool {
return r.fileName != ""
}
func (r *routesConfigImpl) readRoutesConfigFile() (routesConfigStructure, error) {
r.RLock()
defer r.RUnlock()
config := routesConfigStructure{
"",
make(map[string]string),
}
file, fileErr := os.ReadFile(r.fileName)
if fileErr != nil {
return config, errors.Wrap(fileErr, "Could not load the routes config file")
}
parseErr := json.Unmarshal(file, &config)
if parseErr != nil {
return config, errors.Wrap(parseErr, "Could not parse the json routes config file")
}
return config, nil
}
func (r *routesConfigImpl) writeRoutesConfigFile(config routesConfigStructure) error {
r.Lock()
defer r.Unlock()
newFileContent, parseErr := json.Marshal(config)
if parseErr != nil {
return errors.Wrap(parseErr, "Could not parse the routes to json")
}
fileErr := os.WriteFile(r.fileName, newFileContent, 0664)
if fileErr != nil {
return errors.Wrap(fileErr, "Could not write to the routes config file")
}
return nil
}