├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS ├── Makefile ├── README.md ├── VERSION ├── architecture.png ├── bin └── .gitignore ├── config ├── gobetween.json ├── gobetween.toml ├── init │ └── gobetween.conf └── systemd │ └── gobetween.service ├── dist └── .gitignore ├── go.mod ├── go.sum ├── logo.png ├── main.go ├── share ├── discovery │ └── discovery.sh ├── healthcheck │ ├── healthcheck.sh │ └── udp_dns.sh └── rest │ ├── dump.sh │ ├── info.sh │ └── servers │ ├── delete.sh │ ├── get.sh │ ├── list.sh │ └── post.sh ├── snap ├── hooks │ ├── install │ └── remove └── snapcraft.yaml ├── src ├── api │ ├── api.go │ ├── public.go │ ├── root.go │ └── servers.go ├── balance │ ├── iphash.go │ ├── iphash1.go │ ├── leastbandwidth.go │ ├── leastconn.go │ ├── middleware │ │ ├── maxconnections.go │ │ └── sni.go │ ├── registry.go │ ├── roundrobin.go │ └── weight.go ├── cmd │ ├── cmd.go │ ├── from-consul.go │ ├── from-file.go │ ├── from-url.go │ └── root.go ├── config │ └── config.go ├── core │ ├── backend.go │ ├── balancer.go │ ├── context.go │ ├── misc.go │ ├── server.go │ ├── service.go │ └── target.go ├── discovery │ ├── consul.go │ ├── discovery.go │ ├── docker.go │ ├── exec.go │ ├── json.go │ ├── lxd.go │ ├── plaintext.go │ ├── srv.go │ └── static.go ├── go.mod ├── go.sum ├── healthcheck │ ├── exec.go │ ├── healthcheck.go │ ├── ping.go │ ├── probe.go │ └── worker.go ├── info │ └── info.go ├── logging │ └── log.go ├── manager │ └── manager.go ├── metrics │ └── metrics.go ├── server │ ├── modules │ │ └── access │ │ │ ├── access.go │ │ │ └── rule.go │ ├── scheduler │ │ └── scheduler.go │ ├── server.go │ ├── tcp │ │ ├── proxy.go │ │ └── server.go │ └── udp │ │ ├── server.go │ │ └── session │ │ ├── config.go │ │ └── session.go ├── service │ ├── acme.go │ └── service.go ├── stats │ ├── counters │ │ ├── backendscounter.go │ │ ├── bandwidth.go │ │ └── counter.go │ ├── handler.go │ ├── stats.go │ └── store.go └── utils │ ├── codec │ └── codec.go │ ├── env.go │ ├── exec.go │ ├── parsers │ └── backend.go │ ├── pidfile │ └── pidfile.go │ ├── profiler │ └── profiler.go │ ├── proxyprotocol │ └── proxyprotocol.go │ ├── time.go │ └── tls │ ├── sni │ ├── extract.go │ └── sni.go │ └── tls.go └── test ├── dummy_context.go ├── iphash_test.go ├── maxconn_test.go └── weight_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.snap 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.14" 5 | - "stable" 6 | 7 | env: 8 | - GOMAXPROCS=2 9 | 10 | before_install: 11 | - sudo apt-get -qq update 12 | - sudo apt-get install -y zip 13 | 14 | install: 15 | - make deps 16 | 17 | script: 18 | - make dist 19 | 20 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Yaroslav Pogrebnyak 2 | Illarion Kovalchuk 3 | Nick Maliwacki 4 | Nick Doikov 5 | Ievgen Ponomarenko 6 | Shantanu Gadgil 7 | Joe Topjian 8 | David Beck 9 | pitan 10 | chenguoyan 11 | Yousong Zhou 12 | Wilfried Daniels 13 | Tomáš Pospíšek 14 | Tom Morelly 15 | Simon Merschjohann 16 | Seua Polyakov 17 | Nico Schieder 18 | Mike Chepaykin 19 | Michael Schroeder 20 | Knic Knic 21 | Erin 22 | Eric Lindau 23 | Eric Lindau 24 | Chris Williams 25 | Artiom Diomin 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.8.2] 4 | 5 | ### Added 6 | - max_connections parameter for backends to limit connections to individual backends 7 | 8 | ## [0.8.1] 9 | 10 | ### Added 11 | - freebsd/amd64 build 12 | - linux/arm64 build 13 | 14 | ### Updated 15 | - Go to 1.24 16 | - All dependencies to latest versions, security upgrades 17 | - Added tls1.3 option and its cipher suites to TLS configs 18 | 19 | ### Removed 20 | - PreferServerCiphers from TLS configs as it is deprecated in Go 1.24 21 | - SSLv3 as it is deprecated and broken 22 | 23 | 24 | ## [0.8.0] 25 | 26 | ### New Features 27 | - Transparent mode for UDP connections #218 28 | - Environment variables substitution into config (disabled by default) #201 29 | - Combined priority + weight balancing, support of weight = 0 and ignore negative weight #204 30 | - Support of Consul ACL tokens #195 31 | - Pidfile support #190 32 | - TLS probe healthcheck #173 33 | - IPV6 support for SRV discovery #222 34 | 35 | ### Added 36 | - Multistage docker build 37 | - Build as snap package 38 | - Latest letsencrypt changes 39 | 40 | ### Removed 41 | - Windows/386 build 42 | 43 | ### Fixed 44 | - Panic when removing UDP server with enabled access control #215 45 | - Servers deleted via API have scheduler still running #140 46 | - Iphash random selection of backends for the same client ip #229 47 | - Fixed systemd unit #190 48 | - JSON logs format 49 | - High cpu load by UDP server in max_requests=1 mode (fire and forget) by using connection pool #290 50 | - Logging of metric server error #277 51 | 52 | ## [0.7.0] 53 | 54 | ### New Features 55 | - Prometheus Metrics Endpoint 56 | - Improved UDP performance 57 | - Added profiler (otpional, disabled by default) 58 | - Added probe healthceck with different strategies 59 | 60 | 61 | ### Fixed 62 | - CGO Requirement for DNS has been replaced with netgo [#125](https://github.com/yyyar/gobetween/issues/125) 63 | - UDP server was not performing access checks 64 | - Empty `srv_dns_protocol` messed up failpolicy value [#193](https://github.com/yyyar/gobetween/issues/193) 65 | - Fixed missing acme (letsencrypt) tls config during server start [#214](https://github.com/yyyar/gobetween/issues/214) 66 | 67 | 68 | ## [0.6.1] - 2018-10-23 69 | This release brings only bugfixes 70 | 71 | ### Fixed 72 | - No binaries were generated for some of the platforms during make dist 73 | - Regression of roundrobin balancer (it was acting on randomized list of backends) 74 | - Docker image was not working due to missing dynamic library dependencies 75 | - Gobetween became stuck in very rare cases during reading hostname info (sni) from new tls connections. 76 | 77 | 78 | ## [0.6.0] - 2018-08-21 79 | This release brings some improvements and bugfixes. 80 | 81 | ### New Features 82 | - ACME (Letsencrypt) http challenge support (sni challenge is disabled due to security considerations) 83 | 84 | ### Added 85 | - iphash1 algorithm (consistent on backend removal) 86 | - More strict check of UDP server configuration 87 | - /ping public endpoint for healthcheck (PR #127 by Mike Schroeder) 88 | - Support for using the Host Address (PR #123 by David Beck) 89 | - Mentioned gowebhello as an alternative webserver (PR #137 by Shantanu Gadgil) 90 | 91 | ### Fixed 92 | - Fixed iphash algorithm. It was not working properly at all 93 | - Fixed UDP 'session' tracking problems 94 | - Fixed active connections underflow on backend removed and added back, but connections remain established 95 | 96 | ### Changed 97 | - Removed not necessary dependency on libacl1-dev 98 | - Replaced missing dependencies 99 | - Removed lxdhelpers (PR #113 by Joe Topjian) 100 | 101 | 102 | ## [0.5.0] - 2017-10-13 103 | This release brings several new features and various fixes and improvements. 104 | 105 | ### New Features 106 | - ACME (Letsencrypt) protocol support for TLS server 107 | - PROXY protocol v1 support (PR #101 by Nico Schieder) 108 | - LXD Discovery (PR #76 by Joe Topjian) 109 | 110 | 111 | ### Added 112 | - Added more info to server and sni logging errors 113 | - Version number first line to output on startup 114 | - Add sni value to 'not-matching' SNI error message 115 | - Version flags (--version and -v) 116 | - Implemented max requests and responses parameters in UDP 117 | 118 | ### Fixed 119 | - Dns discovery when A records are not presented in additional section of SRV response 120 | - Sni middleware to work fine with default unexpected hostname strategy 121 | - Propagating sni backend value in scheduler after discovery 122 | 123 | ### Changed 124 | - Optimizing Docker image (now FROM scratch) 125 | 126 | 127 | 128 | ## [0.4.0] - 2017-04-07 129 | This release brings many new features and improvements, as well as bugfixes. 130 | Major things are UDP support, TLS termination, TLS proxy, SNI-aware balancing. 131 | 132 | ### New Features 133 | - UDP protocol support 134 | - TLS termination 135 | - TLS proxy (connect to backends with TLS and configurable certs) 136 | - SNI-aware balancing (routing based on hostname from TLS Server Name Indication record) 137 | 138 | ### Added 139 | - Possibility to enable CORS for REST API 140 | 141 | ### Fixed 142 | - Messed up `client_idle_timeout` and `backend_idle_timeout` 143 | - Bugs in balancers: iphash, roundrobin, weight - now work more accurately 144 | - Goroutine/memory leak caused by consul discovery not reusing http client 145 | 146 | ### Changed 147 | - Docker discovery now can have empty TLS certificates. 148 | - Migrated to golang 1.8. Now it's minimal requirement for the build. 149 | 150 | 151 | 152 | ## [0.3.0] - 2016-08-18 153 | This release brings several new features and improvements, as well as bugfixes. Major things are 154 | integrations with Consul, more flexible command-line options and Access control module. 155 | 156 | ### New Features 157 | - Consul Discovery 158 | - Ability to load config not only from file, but also from URL and Consul key-value storage on startup 159 | - More powerful command-line interface 160 | - Leastbandwidth balancing strategy 161 | 162 | ### Added 163 | - Allow passing parameters as GOBETWEEN env variable instead of args 164 | - Possibility to specify format in /dump endpoint (toml or json) 165 | - Refused connections counters for backends 166 | - TCP mode for DNS SRV Discovery 167 | 168 | ### Fixed 169 | - Creating server with the same name via rest api causes api to freeze 170 | - Runtime error when no [default] section is present in config 171 | 172 | ### Changed 173 | - Replaced big.Int with uint64 for simplicity and performance reasons. 174 | 175 | 176 | 177 | ## [0.2.0] - 2016-07-22 178 | This release brings several big features such as full-functional REST API and Stats, as well 179 | as may bugfixes and improvements. All changes are backward-compatible with 0.1.0. 180 | 181 | ### New Features 182 | - REST API implementation (info, servers list/create/remove, stats, config dump). 183 | - Implemented gathering stats for servers and backends (rx/tx, rx/tx per second, connections count, etc) 184 | 185 | ### Added 186 | - Set GOMAXPROCS to cpu count automatically if no env var is present 187 | - Added TLS support for Docker discovery 188 | - Added `docker_container_host_env_var` property to Docker discovery 189 | - Allow any type of value (int or string) in port in JSON discovery 190 | - Make healthchecks optional 191 | 192 | ### Fixed 193 | - Fixed panic runtime error exec discovery when `exec_command` is not valid path and timeout=0 194 | - Fixed roundrobin balance strategy 195 | - Fixed how SRV discovery handler large UDP responses; Fixed sometimes missed port. 196 | - Fixed parsing backend on windows (with \r newlines) 197 | 198 | 199 | ## [0.1.0] - 2016-06-08 200 | ### Added 201 | - Initial project implementation (by @yyyar and @kikom). 202 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Reporting bugs and proposals 4 | 5 | You can [submit an issue](https://github.com/yyyar/gobetween/issues/new). Please be descriptive and provide as much evidence 6 | as you can, so that we could either help you to fix your setup, or reproduce the issue on our side. This includes config of 7 | gobetween, logs, expected behavior and actual behavior, steps to reproduce. 8 | 9 | Please make sure you've cleaned the data you provide us from sensitive information such as public ips, user names, emails, logins, 10 | security tokens and other information that you don't want to make public. 11 | 12 | ## Submitting changes 13 | 14 | Please send a [GitHub Pull Request to gobetween](https://github.com/yyyar/gobetween/pull/new/master) with a clear description 15 | of what you've done. Please make sure all your commits are atomic (one feature per commit). Please follow our coding conventions 16 | 17 | * Prior to submitting PR, make an issue with a question. It could save a lot of time and efforts because we could be already 18 | working on it, or the change you propose does not fit the gobetween ideology. 19 | * In case if you have intermediate commits, such as "WIP - work in progress" or sequence of commits that add some files/code and 20 | then removes it, please [squash](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History) them prior to pull request 21 | * Please make sure git commits are descriptive 22 | * Please change just a required minimum of code in order to implement your feature or fix a bug. Pull requests that have 23 | a lot of not related changes, are hard to review and hard to merge due to possible conflicts with other branches: 24 | * Don't rename existing variables if it's not required for your change 25 | * Don't add or remove empty strings 26 | * Don't autoformat files according to your IDE settings, leave original formatting in places that you don't explicitly change 27 | * Write go-doc style comments on functions you add 28 | 29 | If your not related change is really good -- make a distinct pull request for it. 30 | 31 | 32 | ## Coding conventions 33 | 34 | Please read the code and you'll get the essence of it. Following coding conventions is an attempt to make a short but not 35 | complete extract: 36 | 37 | * Simple code is better than smart tricky code 38 | * Keep performance in mind - every line your write could be hit millions times per second 39 | * Please use [gofmt](https://golang.org/cmd/gofmt/), it's easy to setup gofmt integration with your editor 40 | * Error values should be handled (and wrapped adding description using [fmt.Errorf](https://golang.org/pkg/fmt/#Errorf) 41 | * Don't panic() unless it's an assert to detect logically impossible situations 42 | * We have borrowed 'this' as the name of a struct's method receiver, in order to survive possible struct rename. You can use 43 | both options - 'this' and 'x' where x is the first letter of a struct name. 44 | * Please use common sense 45 | * Have fun! :) 46 | 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=scratch 2 | 3 | # --------------------- dev (build) image --------------------- # 4 | 5 | FROM golang:1.24-alpine as builder 6 | 7 | RUN apk add git 8 | RUN apk add make 9 | 10 | RUN mkdir -p /opt/gobetween 11 | WORKDIR /opt/gobetween 12 | 13 | RUN mkdir ./src 14 | COPY ./src/go.mod ./src/go.mod 15 | COPY ./src/go.sum ./src/go.sum 16 | 17 | COPY go.mod . 18 | COPY go.sum . 19 | 20 | RUN go mod download 21 | 22 | COPY . . 23 | 24 | RUN make build-static 25 | 26 | # --------------------- final image --------------------- # 27 | 28 | FROM $BASE_IMAGE 29 | 30 | WORKDIR / 31 | 32 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 33 | COPY --from=builder /opt/gobetween/bin/gobetween . 34 | 35 | CMD ["/gobetween", "-c", "/etc/gobetween/conf/gobetween.toml"] 36 | 37 | LABEL org.label-schema.vendor="gobetween" \ 38 | org.label-schema.url="http://gobetween.io" \ 39 | org.label-schema.name="gobetween" \ 40 | org.label-schema.description="Modern & minimalistic load balancer for the Сloud era" 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | gobetween 2 | Copyright (c) 2016-2020 Yaroslav Pogrebnyak 3 | Copyright (c) 2016-2020 Illarion Kovalchuk 4 | Copyright (c) 2016-2020 Nick Doikov 5 | Copyright (c) 2016-2020 Ievgen Ponomarenko 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), 9 | to deal in the Software without restriction, including without limitation 10 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | and/or sell copies of the Software, and to permit persons to whom the Software 12 | is furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included 15 | in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 20 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Yaroslav Pogrebnyak 2 | Illarion Kovalchuk 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile 3 | # @author Yaroslav Pogrebnyak 4 | # @author Ievgen Ponomarenko 5 | # 6 | 7 | .PHONY: update clean build build-all run package deploy test authors dist snap 8 | 9 | export GOBIN := ${PWD}/bin 10 | export GO111MODULE=on 11 | 12 | NAME := gobetween 13 | VERSION := $(shell cat VERSION) 14 | REVISION := $(shell git rev-parse HEAD 2>/dev/null) 15 | BRANCH := $(shell git symbolic-ref --short HEAD 2>/dev/null) 16 | 17 | LDFLAGS := \ 18 | -X main.version=${VERSION} \ 19 | -X main.revision=${REVISION} \ 20 | -X main.branch=${BRANCH} 21 | 22 | default: build 23 | 24 | clean: 25 | @echo Cleaning up... 26 | @rm bin/* -rf 27 | @rm dist/* -rf 28 | @echo Done. 29 | 30 | build: 31 | @echo Building... 32 | go build -v -o ./bin/$(NAME) -ldflags '${LDFLAGS}' . 33 | @echo Done. 34 | 35 | build-static: 36 | @echo Building... 37 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -a -tags netgo -o ./bin/$(NAME) -ldflags '-s -w --extldflags "-static" ${LDFLAGS}' . 38 | @echo Done. 39 | 40 | run: build 41 | ./bin/$(NAME) -c ./config/${NAME}.toml 42 | 43 | test: 44 | @go test -v ./test/... 45 | 46 | install: build 47 | install -d ${DESTDIR}/usr/local/bin/ 48 | install -m 755 ./bin/${NAME} ${DESTDIR}/usr/local/bin/${NAME} 49 | install ./config/${NAME}.toml ${DESTDIR}/etc/${NAME}.toml 50 | 51 | uninstall: 52 | rm -f ${DESTDIR}/usr/local/bin/${NAME} 53 | rm -f ${DESTDIR}/etc/${NAME}.toml 54 | 55 | authors: 56 | @git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort | uniq -c -i | sort -nr | sed "s/^ *[0-9]* //g" > AUTHORS 57 | @cat AUTHORS 58 | 59 | deps: 60 | go mod download 61 | 62 | clean-dist: 63 | rm -rf ./dist/${VERSION} 64 | 65 | dist: 66 | @# For linux 386 when building on linux amd64 you'll need 'libc6-dev-i386' package 67 | @echo Building dist 68 | 69 | @set -e ;\ 70 | for arch in "freebsd amd64 0 " \ 71 | "linux 386 0 " \ 72 | "linux amd64 0 " \ 73 | "linux arm64 0 " \ 74 | "linux arm 0 " \ 75 | "darwin amd64 0 " \ 76 | "windows amd64 0 .exe " ; \ 77 | do \ 78 | set -- $$arch ; \ 79 | echo "******************* $$1_$$2 ********************" ;\ 80 | distpath="./dist/${VERSION}/$$1_$$2" ;\ 81 | mkdir -p $$distpath ; \ 82 | CGO_ENABLED=$$3 GOOS=$$1 GOARCH=$$2 go build -v -a -tags netgo -o $$distpath/$(NAME)$$4 -ldflags '-s -w --extldflags "-static" ${LDFLAGS}' . ;\ 83 | cp "README.md" "LICENSE" "CHANGELOG.md" "AUTHORS" $$distpath ;\ 84 | mkdir -p $$distpath/config && cp "./config/gobetween.toml" $$distpath/config ;\ 85 | if [ "$$1" = "linux" ]; then \ 86 | cd $$distpath && tar -zcvf ../../${NAME}_${VERSION}_$$1_$$2.tar.gz * && cd - ;\ 87 | else \ 88 | cd $$distpath && zip -r ../../${NAME}_${VERSION}_$$1_$$2.zip . && cd - ;\ 89 | fi \ 90 | done 91 | 92 | docker: 93 | @echo Building docker container LATEST 94 | docker build -t yyyar/gobetween . 95 | 96 | docker-run: 97 | docker run --rm --net=host \ 98 | -v $(shell pwd)/config:/etc/gobetween/conf \ 99 | yyyar/gobetween:latest 100 | 101 | docker-tagged: 102 | @echo Building docker container ${VERSION} 103 | docker build -t yyyar/gobetween:${VERSION} . 104 | 105 | snap: 106 | @echo Building snap for gobetween ${VERSION} 107 | snapcraft 108 | @echo Done. 109 | @echo Install as service: sudo snap install gobetween_0.8.0+snapshot_amd64.snap --dangerous --classic 110 | @echo Remove: sudo snap remove gobetween 111 | @echo Config file: /var/snap/gobetween/common/gobetween.toml 112 | @echo Override start parameters: /var/snap/gobetween/current/gobetween.sh 113 | 114 | 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://vshymanskyy.github.io/StandWithUkraine) 2 | 3 | gobetween 4 | 5 | [![Tag](https://img.shields.io/github/tag/yyyar/gobetween.svg)](https://github.com/yyyar/gobetween/releases/latest) 6 | [![Build Status](https://travis-ci.org/yyyar/gobetween.svg?branch=master)](https://travis-ci.org/yyyar/gobetween) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/yyyar/gobetween)](https://goreportcard.com/report/github.com/yyyar/gobetween) 8 | [![Docs](https://img.shields.io/badge/docs-current-brightgreen.svg)](https://github.com/yyyar/gobetween/wiki) 9 | [![Docker](https://img.shields.io/docker/pulls/yyyar/gobetween.svg)](https://hub.docker.com/r/yyyar/gobetween/) 10 | [![Telegram](https://img.shields.io/badge/telegram-chat-blue.svg)](https://t.me/joinchat/GdlUlg_gRfchk1BORU82PA) 11 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) 12 | 13 | 14 | **gobetween** - modern & minimalistic load balancer and reverse-proxy for the :cloud: Cloud era. 15 | 16 | **Current status**: *Maintenance mode, accepting PRs*. Currently in use in several highly loaded production environments. 17 | 18 | ## Features 19 | 20 | * [Fast L4 Load Balancing](https://github.com/yyyar/gobetween/wiki) 21 | * **TCP** - with optional [The PROXY Protocol](https://github.com/yyyar/gobetween/wiki/Proxy-Protocol) support 22 | * **TLS** - [TLS Termination](https://github.com/yyyar/gobetween/wiki/Protocols#tls) + [ACME](https://github.com/yyyar/gobetween/wiki/Protocols#tls) & [TLS Proxy](https://github.com/yyyar/gobetween/wiki/Tls-Proxying) 23 | * **UDP** - with optional virtual sessions and transparent mode 24 | 25 | 26 | * [Clear & Flexible Configuration](https://github.com/yyyar/gobetween/wiki/Configuration) with [TOML](config/gobetween.toml) or [JSON](config/gobetween.json) 27 | * **File** - read configuration from the file 28 | * **URL** - query URL by HTTP and get configuration from the response body 29 | * **Consul** - query Consul key-value storage API for configuration 30 | 31 | * [Management REST API](https://github.com/yyyar/gobetween/wiki/REST-API) 32 | * **System Information** - general server info 33 | * **Configuration** - dump current config 34 | * **Servers** - list, create & delete 35 | * **Stats & Metrics** - for servers and backends including rx/tx, status, active connections & etc. 36 | 37 | * [Discovery](https://github.com/yyyar/gobetween/wiki/Discovery) 38 | * **Static** - hardcode backends list in the config file 39 | * **Docker** - query backends from Docker / Swarm API filtered by label 40 | * **Exec** - execute an arbitrary program and get backends from its stdout 41 | * **JSON** - query arbitrary http url and pick backends from response json (of any structure) 42 | * **Plaintext** - query arbitrary http and parse backends from response text with customized regexp 43 | * **SRV** - query DNS server and get backends from SRV records 44 | * **Consul** - query Consul Services API for backends 45 | * **LXD** - query backends from LXD 46 | 47 | * [Healthchecks](https://github.com/yyyar/gobetween/wiki/Healthchecks) 48 | * **Ping** - simple TCP ping healthcheck 49 | * **Exec** - execute arbitrary program passing host & port as options, and read healthcheck status from the stdout 50 | * **Probe** - send specific bytes to backend (udp, tcp or tls) and expect a correct answer (bytes or regexp) 51 | 52 | * [Balancing Strategies](https://github.com/yyyar/gobetween/wiki/Balancing) (with [SNI](https://github.com/yyyar/gobetween/wiki/Server-Name-Indication) and [MaxConnections](https://github.com/yyyar/gobetween/wiki/Balancing#limiting-number-of-active-connections-to-a-backend-since-082) support) 53 | * **Weight** - select backend from pool based relative weights of backends 54 | * **Roundrobin** - simple elect backend from pool in circular order 55 | * **Iphash** - route client to the same backend based on client ip hash 56 | * **Iphash1** - same as iphash but backend removal consistent (clients remain connecting to the same backend, even if some other backends down) 57 | * **Leastconn** - select backend with least active connections 58 | * **Leastbandwidth** - backends with least bandwidth 59 | 60 | * Integrates seamlessly with Docker and with any custom system (thanks to Exec discovery and healthchecks) 61 | 62 | * Single binary distribution 63 | 64 | 65 | ## Architecture 66 | architecture 67 | 68 | ## Usage 69 | 70 | * Install with snap: https://snapcraft.io/gobetween 71 | * [Other Installation Options](https://github.com/yyyar/gobetween/wiki/Installation) 72 | * [Read Configuration Reference](https://github.com/yyyar/gobetween/wiki) 73 | * Execute `gobetween --help` for full help on all available commands and options. 74 | 75 | ## Hacking 76 | 77 | * Install Go 1.24+ https://golang.org/ 78 | * `$ git clone git@github.com:yyyar/gobetween.git` 79 | * `$ make` 80 | * `$ make run` 81 | 82 | ### Debug and Test 83 | Run several web servers for tests in different terminals: 84 | 85 | * `$ python -m SimpleHTTPServer 8000` 86 | * `$ python -m SimpleHTTPServer 8001` 87 | 88 | Instead of Python's internal HTTP module, you can also use a single binary (Go based) webserver like: 89 | https://github.com/udhos/gowebhello 90 | 91 | **gowebhello** has support for SSL sertificates as well (**HTTPS** mode), in case you want to do quick demos 92 | of the **TLS+SNI** capabilities of gobetween. 93 | 94 | Put `localhost:8000` and `localhost:8001` to `static_list` of static discovery in config file, then try it: 95 | 96 | * `$ gobetween -c gobetween.toml` 97 | 98 | * `$ curl http://localhost:3000` 99 | 100 | Enable [profiler](https://blog.golang.org/profiling-go-programs) and debug issues you encounter 101 | ``` 102 | [profiler] 103 | enabled = true # false | true 104 | bind = ":6060" # "host:port" 105 | ``` 106 | 107 | ## Performance 108 | It's Fast! See [Performance Testing](https://github.com/yyyar/gobetween/wiki/Performance-tests) 109 | 110 | ## The Name 111 | It's a play on words: gobetween ("go between"). 112 | 113 | Also, it's written in Go, and it's a proxy so it's something that stays between 2 parties :smile: 114 | 115 | ## License 116 | MIT. See LICENSE file for more details. 117 | 118 | ## Authors & Maintainers 119 | - [Yaroslav Pogrebnyak](http://pogrebnyak.info) 120 | - [Nick Doikov](https://github.com/nickdoikov) 121 | - [Ievgen Ponomarenko](https://github.com/kikom) 122 | - [Illarion Kovalchuk](https://github.com/illarion) 123 | 124 | ## All Contributors 125 | - See [AUTHORS](AUTHORS) 126 | 127 | ## Community 128 | - Join gobetween Telegram group [here](https://t.me/joinchat/GdlUlg_gRfchk1BORU82PA). 129 | 130 | ## Logo 131 | Logo by [Max Demchenko](https://www.linkedin.com/in/max-demchenko-116170112) 132 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.8.2 2 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyyar/gobetween/e469fe460c03e3bb546844f3c56defb04b88d363/architecture.png -------------------------------------------------------------------------------- /bin/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /config/gobetween.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "enabled": true, 4 | "bind": "0.0.0.0:8888" 5 | }, 6 | "metrics": { 7 | "enabled": true, 8 | "bind": ":9284" 9 | }, 10 | "servers": { 11 | "sample":{ 12 | "bind":"localhost:3000", 13 | "healthcheck": { 14 | "kind": "ping", 15 | "interval": "2s", 16 | "timeout": "1s" 17 | }, 18 | "discovery": { 19 | "kind": "static", 20 | "static_list": ["localhost:8000 weight=1"] 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/init/gobetween.conf: -------------------------------------------------------------------------------- 1 | # gobetween service 2 | 3 | description "gobetween" 4 | 5 | env DAEMON=/usr/local/bin/gobetween 6 | env NAME=gobetween 7 | env CONFIG_PATH=/etc/gobetween.toml 8 | 9 | export GOMAXPROCS=`nproc` 10 | 11 | start on runlevel [2345] 12 | stop on runlevel [!2345] 13 | kill signal INT 14 | 15 | respawn 16 | respawn limit 10 5 17 | umask 022 18 | 19 | expect stop 20 | respawn 21 | 22 | script 23 | exec $DAEMON -c $CONFIG_PATH 2>&1 24 | end script 25 | 26 | post-stop script 27 | pid=`pidof gobetween` 28 | kill -9 $pid 29 | end script 30 | 31 | -------------------------------------------------------------------------------- /config/systemd/gobetween.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Gobetween - modern LB for cloud era 3 | Documentation=https://github.com/yyyar/gobetween/wiki 4 | After=network.target remote-fs.target nss-lookup.target 5 | 6 | [Service] 7 | Type=simple 8 | PIDFile=/run/gobetween.pid 9 | #ExecStartPre=prestart some command 10 | ExecStart=/usr/sbin/gobetween -c /etc/gobetween.toml --pidfile /run/gobetween.pid 11 | ExecStop=/bin/kill -s TERM $MAINPID 12 | PrivateTmp=true 13 | LimitNOFILE=infinity 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | 18 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yyyar/gobetween/main 2 | 3 | go 1.24 4 | 5 | replace github.com/yyyar/gobetween => ./src 6 | 7 | require github.com/yyyar/gobetween v0.0.0-20220331192546-6e185295c847 8 | 9 | require ( 10 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 11 | github.com/Microsoft/go-winio v0.6.2 // indirect 12 | github.com/armon/go-metrics v0.4.1 // indirect 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/burntsushi/toml v0.3.1 // indirect 15 | github.com/bytedance/sonic v1.12.8 // indirect 16 | github.com/bytedance/sonic/loader v0.2.3 // indirect 17 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 18 | github.com/cloudwego/base64x v0.1.5 // indirect 19 | github.com/containerd/log v0.1.0 // indirect 20 | github.com/docker/docker v28.1.1+incompatible // indirect 21 | github.com/docker/go-connections v0.5.0 // indirect 22 | github.com/docker/go-units v0.5.0 // indirect 23 | github.com/elgs/gojq v0.0.0-20230628214826-df5c4045598e // indirect 24 | github.com/elgs/gosplitargs v0.0.0-20241205072753-cbd889c0f906 // indirect 25 | github.com/eric-lindau/udpfacade v0.0.0-20190621043444-d8c1c27add16 // indirect 26 | github.com/fatih/color v1.18.0 // indirect 27 | github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect 28 | github.com/fsouza/go-dockerclient v1.12.1 // indirect 29 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 30 | github.com/gin-contrib/cors v1.7.3 // indirect 31 | github.com/gin-contrib/sse v1.0.0 // indirect 32 | github.com/gin-gonic/gin v1.10.0 // indirect 33 | github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect 34 | github.com/go-playground/locales v0.14.1 // indirect 35 | github.com/go-playground/universal-translator v0.18.1 // indirect 36 | github.com/go-playground/validator/v10 v10.25.0 // indirect 37 | github.com/goccy/go-json v0.10.5 // indirect 38 | github.com/gogo/protobuf v1.3.2 // indirect 39 | github.com/golang/protobuf v1.5.4 // indirect 40 | github.com/google/gopacket v1.1.19 // indirect 41 | github.com/gorilla/websocket v1.5.3 // indirect 42 | github.com/hashicorp/consul/api v1.31.1 // indirect 43 | github.com/hashicorp/errwrap v1.1.0 // indirect 44 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 45 | github.com/hashicorp/go-hclog v1.6.3 // indirect 46 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 47 | github.com/hashicorp/go-metrics v0.5.4 // indirect 48 | github.com/hashicorp/go-multierror v1.1.1 // indirect 49 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 50 | github.com/hashicorp/golang-lru v1.0.2 // indirect 51 | github.com/hashicorp/serf v0.10.2 // indirect 52 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 53 | github.com/json-iterator/go v1.1.12 // indirect 54 | github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a // indirect 55 | github.com/juju/persistent-cookiejar v1.0.0 // indirect 56 | github.com/juju/schema v1.2.0 // indirect 57 | github.com/juju/webbrowser v1.0.0 // indirect 58 | github.com/julienschmidt/httprouter v1.3.0 // indirect 59 | github.com/klauspost/compress v1.18.0 // indirect 60 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 61 | github.com/leodido/go-urn v1.4.0 // indirect 62 | github.com/lxc/lxd v0.0.0-20200706202337-814c96fcec74 // indirect 63 | github.com/mattn/go-colorable v0.1.14 // indirect 64 | github.com/mattn/go-isatty v0.0.20 // indirect 65 | github.com/miekg/dns v1.1.63 // indirect 66 | github.com/mitchellh/go-homedir v1.1.0 // indirect 67 | github.com/mitchellh/mapstructure v1.5.0 // indirect 68 | github.com/moby/docker-image-spec v1.3.1 // indirect 69 | github.com/moby/go-archive v0.1.0 // indirect 70 | github.com/moby/patternmatcher v0.6.0 // indirect 71 | github.com/moby/sys/sequential v0.6.0 // indirect 72 | github.com/moby/sys/user v0.4.0 // indirect 73 | github.com/moby/sys/userns v0.1.0 // indirect 74 | github.com/moby/term v0.5.2 // indirect 75 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 76 | github.com/modern-go/reflect2 v1.0.2 // indirect 77 | github.com/morikuni/aec v1.0.0 // indirect 78 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 79 | github.com/opencontainers/go-digest v1.0.0 // indirect 80 | github.com/opencontainers/image-spec v1.1.1 // indirect 81 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 82 | github.com/pires/go-proxyproto v0.8.0 // indirect 83 | github.com/pkg/errors v0.9.1 // indirect 84 | github.com/prometheus/client_golang v1.20.5 // indirect 85 | github.com/prometheus/client_model v0.6.1 // indirect 86 | github.com/prometheus/common v0.62.0 // indirect 87 | github.com/prometheus/procfs v0.15.1 // indirect 88 | github.com/rogpeppe/fastuuid v1.2.0 // indirect 89 | github.com/sirupsen/logrus v1.9.3 // indirect 90 | github.com/spf13/cobra v1.9.1 // indirect 91 | github.com/spf13/pflag v1.0.6 // indirect 92 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 93 | github.com/ugorji/go/codec v1.2.12 // indirect 94 | golang.org/x/arch v0.14.0 // indirect 95 | golang.org/x/crypto v0.37.0 // indirect 96 | golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect 97 | golang.org/x/mod v0.23.0 // indirect 98 | golang.org/x/net v0.35.0 // indirect 99 | golang.org/x/sync v0.13.0 // indirect 100 | golang.org/x/sys v0.32.0 // indirect 101 | golang.org/x/term v0.31.0 // indirect 102 | golang.org/x/text v0.24.0 // indirect 103 | golang.org/x/tools v0.30.0 // indirect 104 | google.golang.org/protobuf v1.36.5 // indirect 105 | gopkg.in/errgo.v1 v1.0.1 // indirect 106 | gopkg.in/httprequest.v1 v1.2.1 // indirect 107 | gopkg.in/juju/environschema.v1 v1.0.1 // indirect 108 | gopkg.in/macaroon-bakery.v2 v2.3.0 // indirect 109 | gopkg.in/macaroon.v2 v2.1.0 // indirect 110 | gopkg.in/retry.v1 v1.0.3 // indirect 111 | gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5 // indirect 112 | gopkg.in/yaml.v2 v2.4.0 // indirect 113 | gopkg.in/yaml.v3 v3.0.1 // indirect 114 | ) 115 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyyar/gobetween/e469fe460c03e3bb546844f3c56defb04b88d363/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /** 2 | * main.go - entry point 3 | * @author Yaroslav Pogrebnyak 4 | */ 5 | package main 6 | 7 | import ( 8 | "log" 9 | "math/rand" 10 | "os" 11 | "runtime" 12 | "time" 13 | 14 | "github.com/yyyar/gobetween/api" 15 | "github.com/yyyar/gobetween/cmd" 16 | "github.com/yyyar/gobetween/config" 17 | "github.com/yyyar/gobetween/info" 18 | "github.com/yyyar/gobetween/logging" 19 | "github.com/yyyar/gobetween/manager" 20 | "github.com/yyyar/gobetween/metrics" 21 | "github.com/yyyar/gobetween/utils/codec" 22 | ) 23 | 24 | /** 25 | * version,revision,branch should be set while build using ldflags (see Makefile) 26 | */ 27 | var ( 28 | version string 29 | revision string 30 | branch string 31 | ) 32 | 33 | /** 34 | * Initialize package 35 | */ 36 | func init() { 37 | 38 | // Set GOMAXPROCS if not set 39 | if os.Getenv("GOMAXPROCS") == "" { 40 | runtime.GOMAXPROCS(runtime.NumCPU()) 41 | } 42 | 43 | // Init random seed 44 | rand.Seed(time.Now().UnixNano()) 45 | 46 | // Save info 47 | info.Version = version 48 | info.Revision = revision 49 | info.Branch = branch 50 | info.StartTime = time.Now() 51 | 52 | } 53 | 54 | /** 55 | * Entry point 56 | */ 57 | func main() { 58 | 59 | log.Printf("gobetween v%s", version) 60 | 61 | env := os.Getenv("GOBETWEEN") 62 | if env != "" && len(os.Args) > 1 { 63 | log.Fatal("Passed GOBETWEEN env var and command-line arguments: only one allowed") 64 | } 65 | 66 | // Try parse env var to args 67 | if env != "" { 68 | a := []string{} 69 | if err := codec.Decode(env, &a, "json"); err != nil { 70 | log.Fatal("Error converting env var to parameters: ", err, " ", env) 71 | } 72 | os.Args = append([]string{""}, a...) 73 | log.Println("Using parameters from env var: ", os.Args) 74 | } 75 | 76 | // Process flags and start 77 | cmd.Execute(func(cfg *config.Config) { 78 | 79 | // Configure logging 80 | logging.Configure(cfg.Logging.Output, cfg.Logging.Level, cfg.Logging.Format) 81 | 82 | // Start manager 83 | manager.Initialize(*cfg) 84 | 85 | /* setup metrics */ 86 | metrics.Start((*cfg).Metrics) 87 | 88 | // Start API 89 | api.Start((*cfg).Api) 90 | 91 | // block forever 92 | <-(chan string)(nil) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /share/discovery/discovery.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # @author Yaroslav Pogrebnyak 4 | # 5 | # Sample script for exec discovery. 6 | # Should write to stdout string in the same format as 7 | # in static discovery separated with newlines. 8 | # No newline needed after the content. 9 | # 10 | # For more info see sample gobetween.toml 11 | # 12 | 13 | echo localhost:8000 weight=1 14 | echo localhost:8001 weight=2 15 | -------------------------------------------------------------------------------- /share/healthcheck/healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # @author Yaroslav Pogrebnyak 4 | # 5 | # Sample script for exec healthcheck 6 | # For more info see sample gobetween.toml 7 | # 8 | # gobetween expects (by default): 9 | # - singe character '1' in output (without newline and quotes) - if healcheck was successfull, 10 | # - singe character '0' in output - if healcheck failed 11 | # - on any other output or script error - no change to backend status will be applied 12 | # It may be overriden in configuration file. 13 | # 14 | # first and second arguments to the script is host and port, it will be called as: 15 | # yourcmd 16 | # 17 | 18 | host=$1 19 | port=$2 20 | 21 | if [[ "$host:$port" = "localhost:8000" ]]; then 22 | echo -n 1 23 | else 24 | echo -n 0 25 | fi 26 | -------------------------------------------------------------------------------- /share/healthcheck/udp_dns.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # @author Illarion Kovalchuk 4 | # 5 | # Sample script for exec healthcheck of dns backends for udp protocol 6 | 7 | host=$1 8 | port=$2 9 | 10 | dig @"$host" -p "$port" +time=1 > /dev/null 2>&1 ; [[ "$?" == "0" ]] && echo -n 1 || echo -n 0 -------------------------------------------------------------------------------- /share/rest/dump.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # List all current servers 4 | # 5 | curl -XGET "http://localhost:8888/dump" 6 | -------------------------------------------------------------------------------- /share/rest/info.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # List all current servers 4 | # 5 | curl -XGET "http://localhost:8888" 6 | -------------------------------------------------------------------------------- /share/rest/servers/delete.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # List all current servers 4 | # 5 | curl -XDELETE "http://localhost:8888/servers/$1" 6 | -------------------------------------------------------------------------------- /share/rest/servers/get.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # List all current servers 4 | # 5 | curl -XGET "http://localhost:8888/servers/$1" 6 | -------------------------------------------------------------------------------- /share/rest/servers/list.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # List all current servers 4 | # 5 | curl -XGET "http://localhost:8888/servers" 6 | -------------------------------------------------------------------------------- /share/rest/servers/post.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Create new server. 4 | # Format should correspond to [servers] entry in TOML file 5 | # 6 | curl -XPOST "http://localhost:8888/servers/$1" --data ' 7 | { 8 | "bind":"localhost:3001", 9 | 10 | "healthcheck": { 11 | "kind": "ping", 12 | "interval": "2s", 13 | "timeout": "1s" 14 | }, 15 | 16 | "discovery": { 17 | "kind": "static", 18 | "static_list": ["localhost:8000"] 19 | } 20 | } 21 | ' 22 | -------------------------------------------------------------------------------- /snap/hooks/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | cp ${SNAP}/config/gobetween.toml ${SNAP_COMMON}/gobetween.toml 4 | 5 | echo '#/usr/bin/env bash 6 | 7 | ${SNAP}/bin/gobetween -c ${SNAP_COMMON}/gobetween.toml 8 | ' >> ${SNAP_DATA}/gobetween.sh 9 | 10 | chmod +rwx ${SNAP_DATA}/gobetween.sh 11 | -------------------------------------------------------------------------------- /snap/hooks/remove: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | rm -f ${SNAP_COMMON}/gobetween.toml 4 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # snapcraft.yaml - Snapcraft file 3 | # @author Yaroslav Pogrebnyak 4 | # 5 | 6 | name: gobetween 7 | version: '1' 8 | version-script: cat VERSION 9 | icon: logo.png 10 | summary: modern & minimalistic load balancer and reverse-proxy for the Cloud era 11 | description: gobetween is free, open-source, modern & minimalistic L4 load balancer and reverse proxy for the Cloud era 12 | confinement: strict 13 | grade: devel 14 | architectures: 15 | - build-on: amd64 16 | run-on: amd64 17 | - build-on: i386 18 | run-on: i386 19 | base: core18 20 | 21 | parts: 22 | gobetween: 23 | plugin: make 24 | source: . 25 | source-type: local 26 | build-packages: 27 | - gcc 28 | - git 29 | makefile: Makefile 30 | artifacts: 31 | - bin/gobetween 32 | override-build: | 33 | snap list | grep go || snap install go --channel=1.12/stable --classic 34 | snapcraftctl build 35 | 36 | config: 37 | plugin: dump 38 | source: . 39 | stage: 40 | - config/gobetween.toml 41 | 42 | apps: 43 | gobetween: 44 | command: bash $SNAP_DATA/gobetween.sh 45 | daemon: simple 46 | plugs: 47 | - network 48 | - network-bind 49 | 50 | -------------------------------------------------------------------------------- /src/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | /** 4 | * api.go - rest api implementation 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "github.com/gin-contrib/cors" 11 | "github.com/gin-gonic/gin" 12 | "github.com/yyyar/gobetween/config" 13 | "github.com/yyyar/gobetween/logging" 14 | ) 15 | 16 | /* gin app */ 17 | var app *gin.Engine 18 | 19 | /** 20 | * Initialize module 21 | */ 22 | func init() { 23 | gin.SetMode(gin.ReleaseMode) 24 | } 25 | 26 | /** 27 | * Starts REST API server 28 | */ 29 | func Start(cfg config.ApiConfig) { 30 | 31 | var log = logging.For("api") 32 | 33 | if !cfg.Enabled { 34 | log.Info("API disabled") 35 | return 36 | } 37 | 38 | log.Info("Starting up API") 39 | 40 | app = gin.New() 41 | 42 | if cfg.Cors { 43 | corsConfig := cors.DefaultConfig() 44 | corsConfig.AllowAllOrigins = true 45 | corsConfig.AllowCredentials = true 46 | corsConfig.AllowMethods = []string{"PUT", "POST", "DELETE", "GET", "OPTIONS"} 47 | corsConfig.AllowHeaders = []string{"Origin", "Authorization"} 48 | 49 | app.Use(cors.New(corsConfig)) 50 | log.Info("API CORS enabled") 51 | } 52 | 53 | r := app.Group("/") 54 | 55 | if cfg.BasicAuth != nil { 56 | log.Info("Using HTTP Basic Auth") 57 | r.Use(gin.BasicAuth(gin.Accounts{ 58 | cfg.BasicAuth.Login: cfg.BasicAuth.Password, 59 | })) 60 | } 61 | 62 | /* attach endpoints */ 63 | attachRoot(r) 64 | attachServers(r) 65 | 66 | /* attach endpoints with no auth */ 67 | p := app.Group("/") 68 | attachPublic(p) 69 | 70 | go func() { 71 | var err error 72 | /* start rest api server */ 73 | if cfg.Tls != nil { 74 | log.Info("Starting HTTPS server ", cfg.Bind) 75 | err = app.RunTLS(cfg.Bind, cfg.Tls.CertPath, cfg.Tls.KeyPath) 76 | } else { 77 | log.Info("Starting HTTP server ", cfg.Bind) 78 | err = app.Run(cfg.Bind) 79 | } 80 | 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | }() 85 | } 86 | -------------------------------------------------------------------------------- /src/api/public.go: -------------------------------------------------------------------------------- 1 | /** 2 | * public.go - / rest api implementation 3 | * 4 | * @author Mike Schroeder 5 | */ 6 | package api 7 | 8 | import ( 9 | "github.com/gin-gonic/gin" 10 | "net/http" 11 | ) 12 | 13 | /** 14 | * Attaches / handlers 15 | */ 16 | func attachPublic(app *gin.RouterGroup) { 17 | 18 | /** 19 | * Simple 200 and OK response 20 | */ 21 | app.GET("/ping", func(c *gin.Context) { 22 | c.String(http.StatusOK, "OK") 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/api/root.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | /** 4 | * root.go - / rest api implementation 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "net/http" 11 | "os" 12 | "time" 13 | 14 | "github.com/gin-gonic/gin" 15 | "github.com/yyyar/gobetween/info" 16 | "github.com/yyyar/gobetween/manager" 17 | ) 18 | 19 | /** 20 | * Attaches / handlers 21 | */ 22 | func attachRoot(app *gin.RouterGroup) { 23 | 24 | /** 25 | * Global stats 26 | */ 27 | app.GET("/", func(c *gin.Context) { 28 | 29 | c.IndentedJSON(http.StatusOK, gin.H{ 30 | "pid": os.Getpid(), 31 | "time": time.Now(), 32 | "startTime": info.StartTime, 33 | "uptime": time.Now().Sub(info.StartTime).String(), 34 | "version": info.Version, 35 | "configuration": info.Configuration, 36 | }) 37 | }) 38 | 39 | /** 40 | * Dump current config as TOML 41 | */ 42 | app.GET("/dump", func(c *gin.Context) { 43 | format := c.DefaultQuery("format", "toml") 44 | 45 | data, err := manager.DumpConfig(format) 46 | if err != nil { 47 | c.IndentedJSON(http.StatusInternalServerError, err.Error()) 48 | return 49 | } 50 | 51 | c.String(http.StatusOK, data) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /src/api/servers.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | /** 4 | * servers.go - /servers rest api implementation 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "net/http" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/yyyar/gobetween/config" 14 | "github.com/yyyar/gobetween/manager" 15 | "github.com/yyyar/gobetween/stats" 16 | ) 17 | 18 | /** 19 | * Attaches /servers handlers 20 | */ 21 | func attachServers(app *gin.RouterGroup) { 22 | 23 | /** 24 | * Find all current configured servers 25 | */ 26 | app.GET("/servers", func(c *gin.Context) { 27 | c.IndentedJSON(http.StatusOK, manager.All()) 28 | }) 29 | 30 | /** 31 | * Find server by name 32 | */ 33 | app.GET("/servers/:name", func(c *gin.Context) { 34 | name := c.Param("name") 35 | c.IndentedJSON(http.StatusOK, manager.Get(name)) 36 | }) 37 | 38 | /** 39 | * Delete server by name 40 | */ 41 | app.DELETE("/servers/:name", func(c *gin.Context) { 42 | name := c.Param("name") 43 | manager.Delete(name) 44 | c.IndentedJSON(http.StatusOK, nil) 45 | }) 46 | 47 | /** 48 | * Create new server with name :name 49 | */ 50 | app.POST("/servers/:name", func(c *gin.Context) { 51 | 52 | name := c.Param("name") 53 | 54 | cfg := config.Server{} 55 | if err := c.BindJSON(&cfg); err != nil { 56 | c.IndentedJSON(http.StatusBadRequest, err.Error()) 57 | return 58 | } 59 | 60 | if err := manager.Create(name, cfg); err != nil { 61 | c.IndentedJSON(http.StatusConflict, err.Error()) 62 | return 63 | } 64 | 65 | c.IndentedJSON(http.StatusOK, nil) 66 | }) 67 | 68 | /** 69 | * Get server stats 70 | */ 71 | app.GET("/servers/:name/stats", func(c *gin.Context) { 72 | name := c.Param("name") 73 | c.IndentedJSON(http.StatusOK, stats.GetStats(name)) 74 | }) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/balance/iphash.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | /** 4 | * iphash.go - iphash balance impl 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "errors" 11 | "hash/fnv" 12 | "sort" 13 | 14 | "github.com/yyyar/gobetween/core" 15 | ) 16 | 17 | /** 18 | * Iphash balancer 19 | */ 20 | type IphashBalancer struct{} 21 | 22 | /** 23 | * Elect backend using iphash strategy 24 | * Using fnv1a for speed 25 | * 26 | * TODO: Improve as needed 27 | */ 28 | func (b *IphashBalancer) Elect(context core.Context, backends []*core.Backend) (*core.Backend, error) { 29 | 30 | if len(backends) == 0 { 31 | return nil, errors.New("Can't elect backend, Backends empty") 32 | } 33 | 34 | sort.SliceStable(backends, func(i, j int) bool { 35 | return backends[i].Target.String() < backends[j].Target.String() 36 | }) 37 | 38 | hash := fnv.New32a() 39 | hash.Write(context.Ip()) 40 | backend := backends[hash.Sum32()%uint32(len(backends))] 41 | 42 | return backend, nil 43 | } 44 | -------------------------------------------------------------------------------- /src/balance/iphash1.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | /** 4 | * iphash1.go - semi-consistent iphash balance impl 5 | * 6 | * @author Illarion Kovalchuk 7 | */ 8 | 9 | import ( 10 | "errors" 11 | "hash/fnv" 12 | 13 | "github.com/yyyar/gobetween/core" 14 | ) 15 | 16 | /** 17 | * Iphash balancer 18 | */ 19 | type Iphash1Balancer struct { 20 | } 21 | 22 | /** 23 | * Elect backend using semi-consistent iphash strategy. This is naive implementation 24 | * using Key+Node Hash Algorithm for stable sharding described at http://kennethxu.blogspot.com/2012/11/sharding-algorithm.html 25 | * It survives removing nodes (removing stability), so that clients connected to backends that have not been removed stay 26 | * untouched. 27 | * 28 | */ 29 | func (b *Iphash1Balancer) Elect(context core.Context, backends []*core.Backend) (*core.Backend, error) { 30 | 31 | if len(backends) == 0 { 32 | return nil, errors.New("Can't elect backend, Backends empty") 33 | } 34 | 35 | var result *core.Backend 36 | { 37 | var bestHash uint32 38 | 39 | for i, backend := range backends { 40 | hasher := fnv.New32a() 41 | hasher.Write(context.Ip()) 42 | hasher.Write([]byte(backend.Address())) 43 | s32 := hasher.Sum32() 44 | if s32 > bestHash { 45 | bestHash = s32 46 | result = backends[i] 47 | } 48 | } 49 | } 50 | 51 | return result, nil 52 | } 53 | -------------------------------------------------------------------------------- /src/balance/leastbandwidth.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | /** 4 | * leastbandwidth.go - leastbandwidth balance impl 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "errors" 11 | 12 | "github.com/yyyar/gobetween/core" 13 | ) 14 | 15 | /** 16 | * Leastbandwidth balancer 17 | */ 18 | type LeastbandwidthBalancer struct{} 19 | 20 | /** 21 | * Elect backend using leastbandwidth strategy 22 | */ 23 | func (b *LeastbandwidthBalancer) Elect(context core.Context, backends []*core.Backend) (*core.Backend, error) { 24 | 25 | if len(backends) == 0 { 26 | return nil, errors.New("Can't elect backend, Backends empty") 27 | } 28 | 29 | least := backends[0] 30 | for _, b := range backends { 31 | if b.Stats.TxSecond+b.Stats.RxSecond < least.Stats.TxSecond+least.Stats.RxSecond { 32 | least = b 33 | } 34 | } 35 | 36 | return least, nil 37 | } 38 | -------------------------------------------------------------------------------- /src/balance/leastconn.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | /** 4 | * leastconn.go - leastconn balance impl 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "errors" 11 | 12 | "github.com/yyyar/gobetween/core" 13 | ) 14 | 15 | /** 16 | * Leastconn balancer 17 | */ 18 | type LeastconnBalancer struct{} 19 | 20 | /** 21 | * Elect backend using roundrobin strategy 22 | */ 23 | func (b *LeastconnBalancer) Elect(context core.Context, backends []*core.Backend) (*core.Backend, error) { 24 | 25 | if len(backends) == 0 { 26 | return nil, errors.New("Can't elect backend, Backends empty") 27 | } 28 | 29 | least := backends[0] 30 | 31 | for key, backend := range backends { 32 | if backend.Stats.ActiveConnections <= least.Stats.ActiveConnections { 33 | least = backends[key] 34 | } 35 | } 36 | 37 | return least, nil 38 | } 39 | -------------------------------------------------------------------------------- /src/balance/middleware/maxconnections.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | /** 4 | * maxconn.go - max connections middleware 5 | */ 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/yyyar/gobetween/core" 11 | "github.com/yyyar/gobetween/logging" 12 | ) 13 | 14 | /** 15 | * MaxConnectionsMiddleware middleware 16 | * Filters out backends that have reached their max_connections limit 17 | */ 18 | type MaxConnectionsMiddleware struct { 19 | Delegate core.Balancer 20 | } 21 | 22 | /** 23 | * Elect backend filtering out backends that have reached max connections 24 | */ 25 | func (b *MaxConnectionsMiddleware) Elect(ctx core.Context, backends []*core.Backend) (*core.Backend, error) { 26 | log := logging.For("balance/middleware/maxconn") 27 | 28 | eligible := make([]*core.Backend, 0, len(backends)) 29 | 30 | for _, backend := range backends { 31 | // Skip backends that have reached their connection limit 32 | if backend.MaxConnections > 0 && backend.Stats.ActiveConnections >= uint(backend.MaxConnections) { 33 | log.Debug("Backend ", backend.Address(), " excluded: active connections (", 34 | backend.Stats.ActiveConnections, ") >= max_connections (", backend.MaxConnections, ")") 35 | continue 36 | } 37 | eligible = append(eligible, backend) 38 | } 39 | 40 | if len(eligible) == 0 { 41 | return nil, errors.New("all backends have reached max connections limit") 42 | } 43 | 44 | return b.Delegate.Elect(ctx, eligible) 45 | } 46 | -------------------------------------------------------------------------------- /src/balance/middleware/sni.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | /** 4 | * sni.go - sni middleware 5 | * 6 | * @author Illarion Kovalchuk 7 | * @author Yaroslav Pogrebnyak 8 | */ 9 | 10 | import ( 11 | "errors" 12 | "regexp" 13 | "strings" 14 | 15 | "github.com/yyyar/gobetween/config" 16 | "github.com/yyyar/gobetween/core" 17 | "github.com/yyyar/gobetween/logging" 18 | ) 19 | 20 | /** 21 | * SniMiddleware middleware delegate 22 | */ 23 | type SniMiddleware struct { 24 | SniConf *config.Sni 25 | Delegate core.Balancer 26 | } 27 | 28 | /** 29 | * Elect backend using sni pre-processing 30 | */ 31 | func (sniBalancer *SniMiddleware) Elect(ctx core.Context, backends []*core.Backend) (*core.Backend, error) { 32 | 33 | /* ------ try find matching to requesedSni backends ------ */ 34 | 35 | matchedBackends := sniBalancer.matchingBackends(ctx.Sni(), backends) 36 | if len(matchedBackends) > 0 { 37 | return sniBalancer.Delegate.Elect(ctx, matchedBackends) 38 | } 39 | 40 | /* ------ if no matched backends, fallback to unexpected hostname strategy ------ */ 41 | 42 | switch sniBalancer.SniConf.UnexpectedHostnameStrategy { 43 | case "reject": 44 | return nil, errors.New("No matching sni [" + ctx.Sni() + "] found, rejecting due to 'reject' unexpected hostname strategy") 45 | 46 | case "any": 47 | return sniBalancer.Delegate.Elect(ctx, backends) 48 | 49 | default: 50 | if ctx.Sni() == "" { 51 | return sniBalancer.Delegate.Elect(ctx, []*core.Backend{}) 52 | } 53 | 54 | // default, select only from backends without any sni 55 | return sniBalancer.Delegate.Elect(ctx, sniBalancer.matchingBackends("", backends)) 56 | } 57 | } 58 | 59 | /** 60 | * Filter out backends that match requestedSni 61 | */ 62 | func (sniBalancer *SniMiddleware) matchingBackends(requestedSni string, backends []*core.Backend) []*core.Backend { 63 | 64 | log := logging.For("balance/middleware/sni") 65 | 66 | var matchedBackends []*core.Backend 67 | 68 | for _, backend := range backends { 69 | 70 | match, err := sniBalancer.matchSni(requestedSni, backend.Sni) 71 | 72 | if err != nil { 73 | log.Error(err) 74 | continue 75 | } 76 | 77 | if match { 78 | matchedBackends = append(matchedBackends, backend) 79 | } 80 | } 81 | 82 | return matchedBackends 83 | } 84 | 85 | /** 86 | * Try match requested sni to actual backend sni 87 | */ 88 | func (sniBalancer *SniMiddleware) matchSni(requestedSni string, backendSni string) (bool, error) { 89 | 90 | sniMatching := sniBalancer.SniConf.HostnameMatchingStrategy 91 | 92 | switch sniMatching { 93 | case "regexp": 94 | 95 | if backendSni == "" && requestedSni != "" { 96 | return false, nil 97 | } 98 | 99 | regexp, err := regexp.Compile(backendSni) 100 | if err != nil { 101 | return false, err 102 | } 103 | 104 | return regexp.MatchString(requestedSni), nil 105 | 106 | case "exact": 107 | return strings.ToLower(requestedSni) == strings.ToLower(backendSni), nil 108 | 109 | default: 110 | return false, errors.New("Unsupported sni matching mechanism: " + sniMatching) 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/balance/registry.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | /** 4 | * registry.go - balancers registry 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "reflect" 11 | 12 | "github.com/yyyar/gobetween/balance/middleware" 13 | "github.com/yyyar/gobetween/config" 14 | "github.com/yyyar/gobetween/core" 15 | ) 16 | 17 | /** 18 | * Type registry of available Balancers 19 | */ 20 | var typeRegistry = make(map[string]reflect.Type) 21 | 22 | /** 23 | * Initialize type registry 24 | */ 25 | func init() { 26 | typeRegistry["leastconn"] = reflect.TypeOf(LeastconnBalancer{}) 27 | typeRegistry["roundrobin"] = reflect.TypeOf(RoundrobinBalancer{}) 28 | typeRegistry["weight"] = reflect.TypeOf(WeightBalancer{}) 29 | typeRegistry["iphash"] = reflect.TypeOf(IphashBalancer{}) 30 | typeRegistry["iphash1"] = reflect.TypeOf(Iphash1Balancer{}) 31 | typeRegistry["leastbandwidth"] = reflect.TypeOf(LeastbandwidthBalancer{}) 32 | } 33 | 34 | /** 35 | * Create new Balancer based on balancing strategy 36 | * Wrap it in middlewares if needed 37 | */ 38 | func New(sniConf *config.Sni, balance string) core.Balancer { 39 | 40 | // Create the base balancer 41 | balancer := reflect.New(typeRegistry[balance]).Elem().Addr().Interface().(core.Balancer) 42 | 43 | // Apply max connections middleware (always applied) 44 | balancer = &middleware.MaxConnectionsMiddleware{ 45 | Delegate: balancer, 46 | } 47 | 48 | // Apply SNI middleware if configured 49 | if sniConf != nil { 50 | balancer = &middleware.SniMiddleware{ 51 | SniConf: sniConf, 52 | Delegate: balancer, 53 | } 54 | } 55 | 56 | return balancer 57 | } 58 | -------------------------------------------------------------------------------- /src/balance/roundrobin.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | /** 4 | * roundrobin.go - roundrobin balance impl 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "errors" 11 | "sort" 12 | 13 | "github.com/yyyar/gobetween/core" 14 | ) 15 | 16 | /** 17 | * Roundrobin balancer 18 | */ 19 | type RoundrobinBalancer struct { 20 | 21 | /* Current backend position */ 22 | current int 23 | } 24 | 25 | /** 26 | * Elect backend using roundrobin strategy 27 | */ 28 | func (b *RoundrobinBalancer) Elect(context core.Context, backends []*core.Backend) (*core.Backend, error) { 29 | 30 | if len(backends) == 0 { 31 | return nil, errors.New("Can't elect backend, Backends empty") 32 | } 33 | 34 | sort.SliceStable(backends, func(i, j int) bool { 35 | return backends[i].Target.String() < backends[j].Target.String() 36 | }) 37 | 38 | if b.current >= len(backends) { 39 | b.current = 0 40 | } 41 | 42 | backend := backends[b.current] 43 | b.current += 1 44 | 45 | return backend, nil 46 | } 47 | -------------------------------------------------------------------------------- /src/balance/weight.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | /** 4 | * weight.go - weight balance impl 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "errors" 11 | "math/rand" 12 | 13 | "github.com/yyyar/gobetween/core" 14 | "github.com/yyyar/gobetween/logging" 15 | ) 16 | 17 | /** 18 | * Weight balancer 19 | */ 20 | type WeightBalancer struct{} 21 | 22 | var log = logging.For("balance/weight") 23 | 24 | /** 25 | * Elect backend based on weight with priority strategy. 26 | * See https://tools.ietf.org/html/rfc2782, Priority and Weight sections 27 | */ 28 | func (b *WeightBalancer) Elect(context core.Context, backends []*core.Backend) (*core.Backend, error) { 29 | 30 | if len(backends) == 0 { 31 | return nil, errors.New("Can't elect backend, Backends empty") 32 | } 33 | 34 | // according to RFC we should use backends with lowest priority 35 | minPriority := backends[0].Priority 36 | // group of backends with priority == minPriority 37 | group := make([]*core.Backend, 0, len(backends)) 38 | // sum of weights in the group 39 | groupSumWeight := 0 40 | 41 | // first pass: find lowest numbered priority and a group of backeds with it 42 | for _, backend := range backends { 43 | 44 | if backend.Priority > minPriority { 45 | continue 46 | } 47 | 48 | if backend.Priority < 0 { 49 | log.Warn("Ignoring invalid backend priority %v, should not be less than 0", backend.Priority) 50 | continue 51 | } 52 | 53 | if backend.Weight < 0 { 54 | log.Warn("Ignoring invalid backend weight %v, should not be less than 0", backend.Weight) 55 | continue 56 | } 57 | 58 | // got new lower (accroding to RFC, lower values are preferred) priority, reset 59 | if backend.Priority < minPriority { 60 | minPriority = backend.Priority 61 | group = make([]*core.Backend, 0, len(backends)) 62 | groupSumWeight = 0 63 | } 64 | 65 | group = append(group, backend) 66 | groupSumWeight += backend.Weight 67 | } 68 | 69 | // corner case #1 -- group of just one backend, simply return 70 | if len(group) == 1 { 71 | return group[0], nil 72 | } 73 | 74 | // corner case #2 -- group of backends with weight 0 (allowed by RFC, but not handled by weight distribution algorithm) 75 | if groupSumWeight == 0 { 76 | return group[rand.Intn(len(group))], nil 77 | } 78 | 79 | r := rand.Intn(groupSumWeight) 80 | pos := 0 81 | 82 | // weight selection algorithm 83 | for _, backend := range group { 84 | pos += backend.Weight 85 | if r >= pos { 86 | continue 87 | } 88 | return backend, nil 89 | } 90 | 91 | return nil, errors.New("Can't elect backend") 92 | } 93 | -------------------------------------------------------------------------------- /src/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /** 4 | * cmd.go - command line runner 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "github.com/yyyar/gobetween/config" 11 | ) 12 | 13 | /** 14 | * App Start function to call after initialization 15 | */ 16 | var start func(*config.Config) 17 | 18 | /** 19 | * Execute processing flags 20 | */ 21 | func Execute(f func(*config.Config)) { 22 | start = f 23 | RootCmd.Execute() 24 | } 25 | -------------------------------------------------------------------------------- /src/cmd/from-consul.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /** 4 | * from-consul.go - pull config from consul and run 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "log" 11 | 12 | consul "github.com/hashicorp/consul/api" 13 | "github.com/spf13/cobra" 14 | "github.com/yyyar/gobetween/config" 15 | "github.com/yyyar/gobetween/info" 16 | "github.com/yyyar/gobetween/utils" 17 | "github.com/yyyar/gobetween/utils/codec" 18 | ) 19 | 20 | /* Parsed options */ 21 | var consulKey string 22 | var consulConfig consul.Config = consul.Config{} 23 | 24 | /** 25 | * Add command 26 | */ 27 | func init() { 28 | 29 | FromConsulCmd.Flags().StringVarP(&consulKey, "key", "k", "gobetween", "Consul Key to pull config from") 30 | FromConsulCmd.Flags().StringVarP(&consulConfig.Scheme, "scheme", "s", "http", "http or https") 31 | 32 | RootCmd.AddCommand(FromConsulCmd) 33 | } 34 | 35 | /** 36 | * FromConsul command 37 | */ 38 | var FromConsulCmd = &cobra.Command{ 39 | Use: "from-consul ", 40 | Short: "Start using config from Consul", 41 | Long: `Start using config from the Consul key-value storage`, 42 | Run: func(cmd *cobra.Command, args []string) { 43 | 44 | if len(args) != 1 { 45 | cmd.Help() 46 | return 47 | } 48 | 49 | consulConfig.Address = args[0] 50 | client, err := consul.NewClient(&consulConfig) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | pair, _, err := client.KV().Get(consulKey, nil) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | if pair == nil { 61 | log.Fatal("Empty value for key " + consulKey) 62 | } 63 | 64 | datastr := string(pair.Value) 65 | if isConfigEnvVars { 66 | datastr = utils.SubstituteEnvVars(datastr) 67 | } 68 | 69 | var cfg config.Config 70 | if err := codec.Decode(datastr, &cfg, format); err != nil { 71 | log.Fatal(err) 72 | } 73 | 74 | info.Configuration = struct { 75 | Kind string `json:"kind"` 76 | Host string `json:"host"` 77 | Key string `json:"key"` 78 | }{"consul", consulConfig.Address, consulKey} 79 | 80 | start(&cfg) 81 | }, 82 | } 83 | -------------------------------------------------------------------------------- /src/cmd/from-file.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /** 4 | * from-file.go - pull config from file and run 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "io/ioutil" 11 | "log" 12 | 13 | "github.com/spf13/cobra" 14 | "github.com/yyyar/gobetween/config" 15 | "github.com/yyyar/gobetween/info" 16 | "github.com/yyyar/gobetween/utils" 17 | "github.com/yyyar/gobetween/utils/codec" 18 | ) 19 | 20 | /** 21 | * Add Root Command 22 | */ 23 | func init() { 24 | RootCmd.AddCommand(FromFileCmd) 25 | } 26 | 27 | /** 28 | * FromFile Command 29 | */ 30 | var FromFileCmd = &cobra.Command{ 31 | Use: "from-file ", 32 | Short: "Start using config from file", 33 | Run: func(cmd *cobra.Command, args []string) { 34 | 35 | if len(args) != 1 { 36 | cmd.Help() 37 | return 38 | } 39 | 40 | data, err := ioutil.ReadFile(args[0]) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | var cfg config.Config 46 | 47 | datastr := string(data) 48 | if isConfigEnvVars { 49 | datastr = utils.SubstituteEnvVars(datastr) 50 | } 51 | 52 | if err = codec.Decode(datastr, &cfg, format); err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | info.Configuration = struct { 57 | Kind string `json:"kind"` 58 | Path string `json:"path"` 59 | }{"file", args[0]} 60 | 61 | start(&cfg) 62 | }, 63 | } 64 | -------------------------------------------------------------------------------- /src/cmd/from-url.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /** 4 | * from-url.go - pull config from url and run 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | 14 | "github.com/spf13/cobra" 15 | "github.com/yyyar/gobetween/config" 16 | "github.com/yyyar/gobetween/info" 17 | "github.com/yyyar/gobetween/utils" 18 | "github.com/yyyar/gobetween/utils/codec" 19 | ) 20 | 21 | /** 22 | * Add command 23 | */ 24 | func init() { 25 | 26 | RootCmd.AddCommand(FromUrlCmd) 27 | } 28 | 29 | /** 30 | * FromUrlCmd command 31 | */ 32 | var FromUrlCmd = &cobra.Command{ 33 | Use: "from-url ", 34 | Short: "Start using config from URL", 35 | Run: func(cmd *cobra.Command, args []string) { 36 | 37 | if len(args) != 1 { 38 | cmd.Help() 39 | return 40 | } 41 | 42 | client := http.Client{} 43 | res, err := client.Get(args[0]) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | defer res.Body.Close() 49 | 50 | // Read response 51 | content, err := ioutil.ReadAll(res.Body) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | datastr := string(content) 57 | if isConfigEnvVars { 58 | datastr = utils.SubstituteEnvVars(datastr) 59 | } 60 | 61 | var cfg config.Config 62 | if err := codec.Decode(datastr, &cfg, format); err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | info.Configuration = struct { 67 | Kind string `json:"kind"` 68 | Url string `json:"url"` 69 | }{"url", args[0]} 70 | 71 | start(&cfg) 72 | }, 73 | } 74 | -------------------------------------------------------------------------------- /src/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /** 4 | * root.go - root cmd emulates from-file TODO: remove when time will come 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "fmt" 11 | "github.com/spf13/cobra" 12 | "github.com/yyyar/gobetween/info" 13 | "github.com/yyyar/gobetween/utils/pidfile" 14 | "os" 15 | ) 16 | 17 | /* Persistent parsed options */ 18 | var format string 19 | 20 | /* Parsed options */ 21 | var configPath string 22 | 23 | /* Pid file path */ 24 | var pidFilePath string 25 | 26 | /* Show version */ 27 | var showVersion bool 28 | 29 | /* Substitute env vars in config or not */ 30 | var isConfigEnvVars bool 31 | 32 | /** 33 | * Add Root Command 34 | */ 35 | func init() { 36 | RootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Print version information and quit") 37 | RootCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to configuration file") 38 | RootCmd.PersistentFlags().StringVarP(&pidFilePath, "pidfile", "p", "", "Write pid to specified file") 39 | RootCmd.PersistentFlags().StringVarP(&format, "format", "f", "toml", "Configuration file format: \"toml\" or \"json\"") 40 | RootCmd.PersistentFlags().BoolVarP(&isConfigEnvVars, "use-config-env-vars", "e", false, "Enable env variables interpretation in config file") 41 | } 42 | 43 | /** 44 | * Root Command 45 | */ 46 | var RootCmd = &cobra.Command{ 47 | Use: "gobetween", 48 | Short: "Modern & minimalistic load balancer for the Cloud era", 49 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 50 | if pidFilePath != "" { 51 | if err := pidfile.WritePidFile(pidFilePath); err != nil { 52 | fmt.Printf("Unable to write pidfile %s: %v\n", pidFilePath, err) 53 | os.Exit(1) 54 | } 55 | } 56 | }, 57 | Run: func(cmd *cobra.Command, args []string) { 58 | 59 | if showVersion { 60 | fmt.Println(info.Version) 61 | return 62 | } 63 | 64 | if configPath == "" { 65 | cmd.Help() 66 | return 67 | } 68 | 69 | FromFileCmd.Run(cmd, []string{configPath}) 70 | }, 71 | } 72 | -------------------------------------------------------------------------------- /src/core/backend.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /** 4 | * backend.go - backend definition 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "fmt" 11 | ) 12 | 13 | /** 14 | * Backend means upstream server 15 | * with all needed associate information 16 | */ 17 | type Backend struct { 18 | Target 19 | Priority int `json:"priority"` 20 | Weight int `json:"weight"` 21 | MaxConnections int `json:"max_connections,omitempty"` 22 | Sni string `json:"sni,omitempty"` 23 | Stats BackendStats `json:"stats"` 24 | } 25 | 26 | /** 27 | * Backend status 28 | */ 29 | type BackendStats struct { 30 | Live bool `json:"live"` 31 | Discovered bool `json:"discovered"` 32 | TotalConnections int64 `json:"total_connections"` 33 | ActiveConnections uint `json:"active_connections"` 34 | RefusedConnections uint64 `json:"refused_connections"` 35 | RxBytes uint64 `json:"rx"` 36 | TxBytes uint64 `json:"tx"` 37 | RxSecond uint `json:"rx_second"` 38 | TxSecond uint `json:"tx_second"` 39 | } 40 | 41 | /** 42 | * Check if backend equal to another 43 | */ 44 | func (this *Backend) EqualTo(other Backend) bool { 45 | return this.Target.EqualTo(other.Target) 46 | } 47 | 48 | /** 49 | * Merge another backend to this one 50 | */ 51 | func (this *Backend) MergeFrom(other Backend) *Backend { 52 | 53 | this.Priority = other.Priority 54 | this.Weight = other.Weight 55 | this.MaxConnections = other.MaxConnections 56 | this.Sni = other.Sni 57 | 58 | return this 59 | } 60 | 61 | /** 62 | * Get backends target address 63 | */ 64 | func (this *Backend) Address() string { 65 | return this.Target.Address() 66 | } 67 | 68 | /** 69 | * String conversion 70 | */ 71 | func (this Backend) String() string { 72 | return fmt.Sprintf("{%s p=%d,w=%d,m=%d,l=%t,a=%d}", 73 | this.Address(), this.Priority, this.Weight, this.MaxConnections, this.Stats.Live, this.Stats.ActiveConnections) 74 | } 75 | -------------------------------------------------------------------------------- /src/core/balancer.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /** 4 | * Balancer interface 5 | */ 6 | type Balancer interface { 7 | 8 | /** 9 | * Elect backend based on Balancer implementation 10 | */ 11 | Elect(Context, []*Backend) (*Backend, error) 12 | } 13 | -------------------------------------------------------------------------------- /src/core/context.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /** 4 | * context.go - proxy context 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import "net" 10 | 11 | type Context interface { 12 | String() string 13 | Ip() net.IP 14 | Port() int 15 | Sni() string 16 | } 17 | 18 | /** 19 | * Proxy tcp context 20 | */ 21 | type TcpContext struct { 22 | Hostname string 23 | /** 24 | * Current client connection 25 | */ 26 | Conn net.Conn 27 | } 28 | 29 | func (t TcpContext) String() string { 30 | return t.Conn.RemoteAddr().String() 31 | } 32 | 33 | func (t TcpContext) Ip() net.IP { 34 | return t.Conn.RemoteAddr().(*net.TCPAddr).IP 35 | } 36 | 37 | func (t TcpContext) Port() int { 38 | return t.Conn.RemoteAddr().(*net.TCPAddr).Port 39 | } 40 | 41 | func (t TcpContext) Sni() string { 42 | return t.Hostname 43 | } 44 | 45 | /* 46 | * Proxy udp context 47 | */ 48 | type UdpContext struct { 49 | 50 | /** 51 | * Current client remote address 52 | */ 53 | ClientAddr net.UDPAddr 54 | } 55 | 56 | func (u UdpContext) String() string { 57 | return u.ClientAddr.String() 58 | } 59 | 60 | func (u UdpContext) Ip() net.IP { 61 | return u.ClientAddr.IP 62 | } 63 | 64 | func (u UdpContext) Port() int { 65 | return u.ClientAddr.Port 66 | } 67 | 68 | func (u UdpContext) Sni() string { 69 | return "" 70 | } 71 | -------------------------------------------------------------------------------- /src/core/misc.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /** 4 | * misc.go 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | /** 10 | * Next r/w operation data counters 11 | */ 12 | type ReadWriteCount struct { 13 | 14 | /* Read bytes count */ 15 | CountRead uint 16 | 17 | /* Write bytes count */ 18 | CountWrite uint 19 | 20 | Target Target 21 | } 22 | 23 | func (this ReadWriteCount) IsZero() bool { 24 | return this.CountRead == 0 && this.CountWrite == 0 25 | } 26 | -------------------------------------------------------------------------------- /src/core/server.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /** 4 | * server.go - server 5 | * 6 | * @author Illarion Kovalchuk 7 | * @author Yaroslav Pogrebnyak 8 | */ 9 | 10 | import ( 11 | "github.com/yyyar/gobetween/config" 12 | ) 13 | 14 | /** 15 | * Server interface 16 | */ 17 | type Server interface { 18 | 19 | /** 20 | * Start server 21 | */ 22 | Start() error 23 | 24 | /** 25 | * Stop server and wait until it stop 26 | */ 27 | Stop() 28 | 29 | /** 30 | * Get server configuration 31 | */ 32 | Cfg() config.Server 33 | } 34 | -------------------------------------------------------------------------------- /src/core/service.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /** 4 | * Service is a global facility that could be Enabled or Disabled for a number 5 | * of core.Server instances, depending on their configration. See services/registry 6 | * for exact examples. 7 | */ 8 | type Service interface { 9 | /** 10 | * Enable service for Server 11 | */ 12 | Enable(Server) error 13 | 14 | /** 15 | * Disable service for Server 16 | */ 17 | Disable(Server) error 18 | } 19 | -------------------------------------------------------------------------------- /src/core/target.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /** 4 | * target.go - backend target 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | /** 10 | * Target host and port 11 | */ 12 | type Target struct { 13 | Host string `json:"host"` 14 | Port string `json:"port"` 15 | } 16 | 17 | /** 18 | * Compare to other target 19 | */ 20 | func (t *Target) EqualTo(other Target) bool { 21 | return t.Host == other.Host && 22 | t.Port == other.Port 23 | } 24 | 25 | /** 26 | * Get target full address 27 | * host:port 28 | */ 29 | func (this *Target) Address() string { 30 | return this.Host + ":" + this.Port 31 | } 32 | 33 | /** 34 | * To String conversion 35 | */ 36 | func (this *Target) String() string { 37 | return this.Address() 38 | } 39 | -------------------------------------------------------------------------------- /src/discovery/consul.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | /** 4 | * consul.go - Consul API discovery implementation 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "fmt" 11 | "net/http" 12 | "strings" 13 | "time" 14 | 15 | consul "github.com/hashicorp/consul/api" 16 | "github.com/yyyar/gobetween/config" 17 | "github.com/yyyar/gobetween/core" 18 | "github.com/yyyar/gobetween/logging" 19 | "github.com/yyyar/gobetween/utils" 20 | ) 21 | 22 | const ( 23 | consulRetryWaitDuration = 2 * time.Second 24 | consulTimeout = 2 * time.Second 25 | ) 26 | 27 | /** 28 | * Create new Discovery with Consul fetch func 29 | */ 30 | func NewConsulDiscovery(cfg config.DiscoveryConfig) interface{} { 31 | 32 | d := Discovery{ 33 | opts: DiscoveryOpts{consulRetryWaitDuration}, 34 | fetch: consulFetch, 35 | cfg: cfg, 36 | } 37 | 38 | return &d 39 | } 40 | 41 | /** 42 | * Fetch backends from Consul API 43 | */ 44 | func consulFetch(cfg config.DiscoveryConfig) (*[]core.Backend, error) { 45 | 46 | log := logging.For("consulFetch") 47 | 48 | log.Info("Fetching ", cfg) 49 | 50 | // Prepare vars for http client 51 | // TODO move http & consul client creation to constructor 52 | scheme := "http" 53 | transport := &http.Transport{ 54 | DisableKeepAlives: true, 55 | } 56 | 57 | // Enable tls if needed 58 | if cfg.ConsulTlsEnabled { 59 | tlsConfig := &consul.TLSConfig{ 60 | Address: cfg.ConsulHost, 61 | CertFile: cfg.ConsulTlsCertPath, 62 | KeyFile: cfg.ConsulTlsKeyPath, 63 | CAFile: cfg.ConsulTlsCacertPath, 64 | } 65 | tlsClientConfig, err := consul.SetupTLSConfig(tlsConfig) 66 | if err != nil { 67 | return nil, err 68 | } 69 | transport.TLSClientConfig = tlsClientConfig 70 | scheme = "https" 71 | } 72 | 73 | // Parse http timeout 74 | timeout := utils.ParseDurationOrDefault(cfg.Timeout, consulTimeout) 75 | 76 | // Create consul client 77 | client, _ := consul.NewClient(&consul.Config{ 78 | Token: cfg.ConsulAclToken, 79 | Scheme: scheme, 80 | Address: cfg.ConsulHost, 81 | Datacenter: cfg.ConsulDatacenter, 82 | HttpAuth: &consul.HttpBasicAuth{ 83 | Username: cfg.ConsulAuthUsername, 84 | Password: cfg.ConsulAuthPassword, 85 | }, 86 | HttpClient: &http.Client{Timeout: timeout, Transport: transport}, 87 | }) 88 | 89 | // Query service 90 | service, _, err := client.Health().Service(cfg.ConsulServiceName, cfg.ConsulServiceTag, cfg.ConsulServicePassingOnly, nil) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | // Gather backends 96 | backends := []core.Backend{} 97 | for _, entry := range service { 98 | s := entry.Service 99 | sni := "" 100 | 101 | for _, tag := range s.Tags { 102 | split := strings.SplitN(tag, "=", 2) 103 | 104 | if len(split) != 2 { 105 | continue 106 | } 107 | 108 | if split[0] != "sni" { 109 | continue 110 | } 111 | sni = split[1] 112 | } 113 | 114 | var host string 115 | if s.Address != "" { 116 | host = s.Address 117 | } else { 118 | host = entry.Node.Address 119 | } 120 | 121 | backends = append(backends, core.Backend{ 122 | Target: core.Target{ 123 | Host: host, 124 | Port: fmt.Sprintf("%v", s.Port), 125 | }, 126 | Priority: 1, 127 | Weight: 1, 128 | Stats: core.BackendStats{ 129 | Live: true, 130 | }, 131 | Sni: sni, 132 | }) 133 | } 134 | 135 | return &backends, nil 136 | } 137 | -------------------------------------------------------------------------------- /src/discovery/discovery.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | /** 4 | * discovery.go - discovery 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/yyyar/gobetween/config" 13 | "github.com/yyyar/gobetween/core" 14 | "github.com/yyyar/gobetween/logging" 15 | ) 16 | 17 | /** 18 | * Registry of factory methods for Discoveries 19 | */ 20 | var registry = make(map[string]func(config.DiscoveryConfig) interface{}) 21 | 22 | /** 23 | * Initialize type registry 24 | */ 25 | func init() { 26 | registry["static"] = NewStaticDiscovery 27 | registry["srv"] = NewSrvDiscovery 28 | registry["docker"] = NewDockerDiscovery 29 | registry["json"] = NewJsonDiscovery 30 | registry["exec"] = NewExecDiscovery 31 | registry["plaintext"] = NewPlaintextDiscovery 32 | registry["consul"] = NewConsulDiscovery 33 | registry["lxd"] = NewLXDDiscovery 34 | } 35 | 36 | /** 37 | * Create new Discovery based on strategy 38 | */ 39 | func New(strategy string, cfg config.DiscoveryConfig) *Discovery { 40 | return registry[strategy](cfg).(*Discovery) 41 | } 42 | 43 | /** 44 | * Fetch func for pullig backends 45 | */ 46 | type FetchFunc func(config.DiscoveryConfig) (*[]core.Backend, error) 47 | 48 | /** 49 | * Options for pull discovery 50 | */ 51 | type DiscoveryOpts struct { 52 | RetryWaitDuration time.Duration 53 | } 54 | 55 | /** 56 | * Discovery 57 | */ 58 | type Discovery struct { 59 | 60 | /** 61 | * Cached backends 62 | */ 63 | backends *[]core.Backend 64 | 65 | /** 66 | * Function to fetch / discovery backends 67 | */ 68 | fetch FetchFunc 69 | 70 | /** 71 | * Options for fetch 72 | */ 73 | opts DiscoveryOpts 74 | 75 | /** 76 | * Discovery configuration 77 | */ 78 | cfg config.DiscoveryConfig 79 | 80 | /** 81 | * Channel where to push newly discovered backends 82 | */ 83 | out chan ([]core.Backend) 84 | 85 | /** 86 | * Channel for stopping discovery 87 | */ 88 | stop chan bool 89 | } 90 | 91 | /** 92 | * Pull / fetch backends loop 93 | */ 94 | func (this *Discovery) Start() { 95 | 96 | log := logging.For("discovery") 97 | 98 | this.out = make(chan []core.Backend) 99 | this.stop = make(chan bool) 100 | 101 | // Prepare interval 102 | interval, err := time.ParseDuration(this.cfg.Interval) 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | 107 | // TODO: rewrite with channels for stop 108 | go func() { 109 | for { 110 | backends, err := this.fetch(this.cfg) 111 | 112 | select { 113 | case <-this.stop: 114 | log.Info("Stopping discovery ", this.cfg) 115 | return 116 | default: 117 | } 118 | 119 | if err != nil { 120 | log.Error(this.cfg.Kind, " error ", err, " retrying in ", this.opts.RetryWaitDuration.String()) 121 | log.Info("Applying failpolicy ", this.cfg.Failpolicy) 122 | 123 | if this.cfg.Failpolicy == "setempty" { 124 | this.backends = &[]core.Backend{} 125 | if !this.send() { 126 | log.Info("Stopping discovery ", this.cfg) 127 | return 128 | } 129 | } 130 | 131 | if !this.wait(this.opts.RetryWaitDuration) { 132 | log.Info("Stopping discovery ", this.cfg) 133 | return 134 | } 135 | 136 | continue 137 | } 138 | 139 | // cache 140 | this.backends = backends 141 | if !this.send() { 142 | log.Info("Stopping discovery ", this.cfg) 143 | return 144 | } 145 | 146 | // exit gorouting if no cacheTtl 147 | // used for static discovery 148 | if interval == 0 { 149 | return 150 | } 151 | 152 | if !this.wait(interval) { 153 | log.Info("Stopping discovery ", this.cfg) 154 | return 155 | } 156 | } 157 | }() 158 | } 159 | 160 | func (this *Discovery) send() bool { 161 | // out if not stopped 162 | select { 163 | case <-this.stop: 164 | return false 165 | default: 166 | this.out <- *this.backends 167 | return true 168 | } 169 | } 170 | 171 | /** 172 | * wait waits for interval or stop 173 | * returns true if waiting was successfull 174 | * return false if waiting was interrupted with stop 175 | */ 176 | func (this *Discovery) wait(interval time.Duration) bool { 177 | 178 | t := time.NewTimer(interval) 179 | 180 | select { 181 | case <-t.C: 182 | return true 183 | 184 | case <-this.stop: 185 | if !t.Stop() { 186 | <-t.C 187 | } 188 | return false 189 | } 190 | 191 | } 192 | 193 | /** 194 | * Stop discovery 195 | */ 196 | func (this *Discovery) Stop() { 197 | this.stop <- true 198 | } 199 | 200 | /** 201 | * Returns backends channel 202 | */ 203 | func (this *Discovery) Discover() <-chan []core.Backend { 204 | return this.out 205 | } 206 | -------------------------------------------------------------------------------- /src/discovery/docker.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | /** 4 | * docker.go - Docker API discovery implementation 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "regexp" 13 | "time" 14 | 15 | docker "github.com/fsouza/go-dockerclient" 16 | "github.com/yyyar/gobetween/config" 17 | "github.com/yyyar/gobetween/core" 18 | "github.com/yyyar/gobetween/logging" 19 | "github.com/yyyar/gobetween/utils" 20 | ) 21 | 22 | const ( 23 | dockerRetryWaitDuration = 2 * time.Second 24 | dockerTimeout = 5 * time.Second 25 | ) 26 | 27 | /** 28 | * Create new Discovery with Docker fetch func 29 | */ 30 | func NewDockerDiscovery(cfg config.DiscoveryConfig) interface{} { 31 | 32 | d := Discovery{ 33 | opts: DiscoveryOpts{dockerRetryWaitDuration}, 34 | fetch: dockerFetch, 35 | cfg: cfg, 36 | } 37 | 38 | return &d 39 | } 40 | 41 | /** 42 | * Fetch backends from Docker API 43 | */ 44 | func dockerFetch(cfg config.DiscoveryConfig) (*[]core.Backend, error) { 45 | 46 | log := logging.For("dockerFetch") 47 | 48 | log.Info("Fetching ", cfg.DockerEndpoint, " ", cfg.DockerContainerLabel, " ", cfg.DockerContainerPrivatePort) 49 | 50 | var client *docker.Client 51 | var err error 52 | 53 | if cfg.DockerTlsEnabled { 54 | 55 | // Client cert and key files should be specified together (or both not specified) 56 | // Ca cert may be not specified, so not checked here 57 | if (cfg.DockerTlsCertPath == "") != (cfg.DockerTlsKeyPath == "") { 58 | return nil, errors.New("Missing key or certificate required for TLS client validation") 59 | } 60 | 61 | client, err = docker.NewTLSClient(cfg.DockerEndpoint, cfg.DockerTlsCertPath, cfg.DockerTlsKeyPath, cfg.DockerTlsCacertPath) 62 | 63 | } else { 64 | client, err = docker.NewClient(cfg.DockerEndpoint) 65 | } 66 | 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | /* Set timeout */ 72 | client.HTTPClient.Timeout = utils.ParseDurationOrDefault(cfg.Timeout, dockerTimeout) 73 | 74 | /* Add filter labels if any */ 75 | var filters map[string][]string 76 | if cfg.DockerContainerLabel != "" { 77 | filters = map[string][]string{"label": []string{cfg.DockerContainerLabel}} 78 | } 79 | 80 | /* Fetch containers */ 81 | containers, err := client.ListContainers(docker.ListContainersOptions{Filters: filters}) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | /* Create backends from response */ 87 | 88 | backends := []core.Backend{} 89 | 90 | for _, container := range containers { 91 | for _, port := range container.Ports { 92 | 93 | if port.PrivatePort != cfg.DockerContainerPrivatePort { 94 | continue 95 | } 96 | 97 | containerHost := dockerDetermineContainerHost(client, container.ID, cfg, port.IP) 98 | 99 | backends = append(backends, core.Backend{ 100 | Target: core.Target{ 101 | Host: containerHost, 102 | Port: fmt.Sprintf("%v", port.PublicPort), 103 | }, 104 | Priority: 1, 105 | Weight: 1, 106 | Stats: core.BackendStats{ 107 | Live: true, 108 | }, 109 | Sni: container.Labels["sni"], 110 | }) 111 | } 112 | } 113 | 114 | return &backends, nil 115 | } 116 | 117 | /** 118 | * Determines container host 119 | */ 120 | func dockerDetermineContainerHost(client *docker.Client, id string, cfg config.DiscoveryConfig, portHost string) string { 121 | 122 | log := logging.For("dockerDetermineContainerHost") 123 | 124 | /* If host env var specified, try to get it from container vars */ 125 | 126 | if cfg.DockerContainerHostEnvVar != "" { 127 | 128 | container, err := client.InspectContainer(id) 129 | 130 | if err != nil { 131 | log.Warn(err) 132 | } else { 133 | var e docker.Env = container.Config.Env 134 | h := e.Get(cfg.DockerContainerHostEnvVar) 135 | if h != "" { 136 | return h 137 | } 138 | } 139 | } 140 | 141 | /* If container portHost is not 'all interfaces', return it since it's good enough */ 142 | 143 | if portHost != "0.0.0.0" { 144 | return portHost 145 | } 146 | 147 | /* Last chance, try to parse docker host from endpoint string */ 148 | 149 | var reg = regexp.MustCompile("(.*?)://(?P[-.A-Za-z0-9]+)/?(.*)") 150 | match := reg.FindStringSubmatch(cfg.DockerEndpoint) 151 | 152 | if len(match) == 0 { 153 | return portHost 154 | } 155 | 156 | result := make(map[string]string) 157 | 158 | // get named capturing groups 159 | for i, name := range reg.SubexpNames() { 160 | if name != "" { 161 | result[name] = match[i] 162 | } 163 | } 164 | 165 | h, ok := result["host"] 166 | if !ok { 167 | return portHost 168 | } 169 | 170 | return h 171 | } 172 | -------------------------------------------------------------------------------- /src/discovery/exec.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | /** 4 | * exec.go - Exec external process discovery implementation 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | * @author Ievgen Ponomarenko 8 | */ 9 | 10 | import ( 11 | "strings" 12 | "time" 13 | 14 | "github.com/yyyar/gobetween/config" 15 | "github.com/yyyar/gobetween/core" 16 | "github.com/yyyar/gobetween/logging" 17 | "github.com/yyyar/gobetween/utils" 18 | "github.com/yyyar/gobetween/utils/parsers" 19 | ) 20 | 21 | const ( 22 | execRetryWaitDuration = 2 * time.Second 23 | execResponseWaitTimeout = 3 * time.Second 24 | ) 25 | 26 | /** 27 | * Create new Discovery with Exec fetch func 28 | */ 29 | func NewExecDiscovery(cfg config.DiscoveryConfig) interface{} { 30 | 31 | d := Discovery{ 32 | opts: DiscoveryOpts{execRetryWaitDuration}, 33 | fetch: execFetch, 34 | cfg: cfg, 35 | } 36 | 37 | return &d 38 | } 39 | 40 | /** 41 | * Fetch / refresh backends exec process 42 | */ 43 | func execFetch(cfg config.DiscoveryConfig) (*[]core.Backend, error) { 44 | 45 | log := logging.For("execFetch") 46 | 47 | log.Info("Fetching ", cfg.ExecCommand) 48 | 49 | timeout := utils.ParseDurationOrDefault(cfg.Timeout, execResponseWaitTimeout) 50 | out, err := utils.ExecTimeout(timeout, cfg.ExecCommand...) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | backends := []core.Backend{} 56 | 57 | for _, line := range strings.Split(string(out), "\n") { 58 | 59 | if line == "" { 60 | continue 61 | } 62 | 63 | backend, err := parsers.ParseBackendDefault(line) 64 | if err != nil { 65 | log.Warn(err) 66 | continue 67 | } 68 | 69 | backends = append(backends, *backend) 70 | } 71 | 72 | log.Info("Fetched ", backends) 73 | 74 | return &backends, nil 75 | } 76 | -------------------------------------------------------------------------------- /src/discovery/json.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | /** 4 | * docker.go - Docker API discovery implementation 5 | * 6 | * @author Ievgen Ponomarenko 7 | * @author Yaroslav Pogrebnyak 8 | */ 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "io/ioutil" 14 | "net/http" 15 | "strconv" 16 | "time" 17 | 18 | "github.com/elgs/gojq" 19 | "github.com/yyyar/gobetween/config" 20 | "github.com/yyyar/gobetween/core" 21 | "github.com/yyyar/gobetween/logging" 22 | "github.com/yyyar/gobetween/utils" 23 | ) 24 | 25 | const ( 26 | jsonRetryWaitDuration = 2 * time.Second 27 | jsonDefaultHttpTimeout = 5 * time.Second 28 | jsonDefaultHostPattern = "host" 29 | jsonDefaultPortPattern = "port" 30 | jsonDefaultWeightPattern = "weight" 31 | jsonDefaultPriorityPattern = "priority" 32 | jsonDefaultSniPattern = "sni" 33 | jsonDefaultMaxConnectionsPattern = "max_connections" 34 | ) 35 | 36 | /** 37 | * Create new Discovery with Json fetch func 38 | */ 39 | func NewJsonDiscovery(cfg config.DiscoveryConfig) interface{} { 40 | 41 | /* replace with defaults if needed */ 42 | 43 | if cfg.JsonHostPattern == "" { 44 | cfg.JsonHostPattern = jsonDefaultHostPattern 45 | } 46 | 47 | if cfg.JsonPortPattern == "" { 48 | cfg.JsonPortPattern = jsonDefaultPortPattern 49 | } 50 | 51 | if cfg.JsonWeightPattern == "" { 52 | cfg.JsonWeightPattern = jsonDefaultWeightPattern 53 | } 54 | 55 | if cfg.JsonPriorityPattern == "" { 56 | cfg.JsonPriorityPattern = jsonDefaultPriorityPattern 57 | } 58 | 59 | if cfg.JsonSniPattern == "" { 60 | cfg.JsonSniPattern = jsonDefaultSniPattern 61 | } 62 | 63 | if cfg.JsonMaxConnectionsPattern == "" { 64 | cfg.JsonMaxConnectionsPattern = jsonDefaultMaxConnectionsPattern 65 | } 66 | 67 | d := Discovery{ 68 | opts: DiscoveryOpts{jsonRetryWaitDuration}, 69 | fetch: jsonFetch, 70 | cfg: cfg, 71 | } 72 | 73 | return &d 74 | } 75 | 76 | /** 77 | * Fetch / refresh backends from URL with json in response 78 | */ 79 | func jsonFetch(cfg config.DiscoveryConfig) (*[]core.Backend, error) { 80 | 81 | log := logging.For("jsonFetch") 82 | 83 | log.Info("fetching ", cfg.JsonEndpoint) 84 | 85 | // Make request 86 | timeout := utils.ParseDurationOrDefault(cfg.Timeout, jsonDefaultHttpTimeout) 87 | client := http.Client{Timeout: timeout} 88 | res, err := client.Get(cfg.JsonEndpoint) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | defer res.Body.Close() 94 | 95 | // Read response 96 | content, err := ioutil.ReadAll(res.Body) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | // Build query 102 | parsed, err := gojq.NewStringQuery(string(content)) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | // parse query to array to ensure right format and get length of it 108 | parsedArray, err := parsed.QueryToArray(".") 109 | if err != nil { 110 | return nil, errors.New("Unexpected json in response") 111 | } 112 | 113 | var backends []core.Backend 114 | 115 | for k := range parsedArray { 116 | 117 | var key = "[" + strconv.Itoa(k) + "]." 118 | 119 | backend := core.Backend{ 120 | Weight: 1, 121 | Priority: 1, 122 | Stats: core.BackendStats{ 123 | Live: true, 124 | }, 125 | } 126 | 127 | if backend.Host, err = parsed.QueryToString(key + cfg.JsonHostPattern); err != nil { 128 | return nil, err 129 | } 130 | 131 | // workaround to allow string or number port value 132 | port, err := parsed.Query(key + cfg.JsonPortPattern) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | // convert port to string (if not) 138 | backend.Port = fmt.Sprintf("%v", port) 139 | 140 | if weight, err := parsed.QueryToInt64(key + cfg.JsonWeightPattern); err == nil { 141 | backend.Weight = int(weight) 142 | } 143 | 144 | if priority, err := parsed.QueryToFloat64(key + cfg.JsonPriorityPattern); err == nil { 145 | backend.Priority = int(priority) 146 | } 147 | 148 | if sni, err := parsed.QueryToString(key + cfg.JsonSniPattern); err == nil { 149 | backend.Sni = sni 150 | } 151 | 152 | if maxConnections, err := parsed.QueryToInt64(key + cfg.JsonMaxConnectionsPattern); err == nil { 153 | backend.MaxConnections = int(maxConnections) 154 | } 155 | 156 | backends = append(backends, backend) 157 | } 158 | 159 | log.Info(backends) 160 | 161 | return &backends, nil 162 | } 163 | -------------------------------------------------------------------------------- /src/discovery/lxd.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | /** 4 | * lxd.go - LXD API discovery implementation 5 | * 6 | * @author Joe Topjian 7 | */ 8 | 9 | import ( 10 | "encoding/pem" 11 | "fmt" 12 | "os" 13 | "strings" 14 | "time" 15 | 16 | lxd "github.com/lxc/lxd/client" 17 | lxd_config "github.com/lxc/lxd/lxc/config" 18 | "github.com/lxc/lxd/shared" 19 | lxd_api "github.com/lxc/lxd/shared/api" 20 | "github.com/yyyar/gobetween/config" 21 | "github.com/yyyar/gobetween/core" 22 | "github.com/yyyar/gobetween/logging" 23 | "github.com/yyyar/gobetween/utils" 24 | ) 25 | 26 | const ( 27 | lxdRetryWaitDuration = 2 * time.Second 28 | lxdTimeout = 5 * time.Second 29 | ) 30 | 31 | /** 32 | * Create new Discovery with LXD fetch func 33 | */ 34 | func NewLXDDiscovery(cfg config.DiscoveryConfig) interface{} { 35 | 36 | d := Discovery{ 37 | opts: DiscoveryOpts{lxdRetryWaitDuration}, 38 | fetch: lxdFetch, 39 | cfg: cfg, 40 | } 41 | 42 | return &d 43 | } 44 | 45 | /** 46 | * Fetch backends from LXD API 47 | */ 48 | func lxdFetch(cfg config.DiscoveryConfig) (*[]core.Backend, error) { 49 | log := logging.For("lxdFetch") 50 | 51 | /* Get an LXD client */ 52 | client, err := lxdBuildClient(cfg) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | /* Get an LXD config */ 58 | lxdConfig, err := lxdBuildConfig(cfg) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | /* Set the timeout for the client */ 64 | httpClient, err := client.GetHTTPClient() 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | httpClient.Timeout = utils.ParseDurationOrDefault(cfg.Timeout, lxdTimeout) 70 | 71 | log.Debug("Fetching containers from ", lxdConfig.Remotes[cfg.LXDServerRemoteName].Addr) 72 | 73 | /* Create backends from response */ 74 | backends := []core.Backend{} 75 | 76 | /* Fetch containers */ 77 | containers, err := client.GetContainers() 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | for _, container := range containers { 83 | 84 | /* Ignore containers that aren't running */ 85 | if container.Status != "Running" { 86 | continue 87 | } 88 | 89 | /* Ignore continers if not match label key and value */ 90 | if cfg.LXDContainerLabelKey != "" { 91 | 92 | actualLabelValue, ok := container.Config[cfg.LXDContainerLabelKey] 93 | if !ok { 94 | continue 95 | } 96 | 97 | if cfg.LXDContainerLabelValue != "" && actualLabelValue != cfg.LXDContainerLabelValue { 98 | continue 99 | } 100 | } 101 | 102 | /* Try get container port either from label, or from discovery config */ 103 | port := fmt.Sprintf("%v", cfg.LXDContainerPort) 104 | 105 | if cfg.LXDContainerPortKey != "" { 106 | if p, ok := container.Config[cfg.LXDContainerPortKey]; ok { 107 | port = p 108 | } 109 | } 110 | 111 | if port == "" { 112 | log.Warn(fmt.Sprintf("Port is not found in neither in lxd_container_port config not in %s label for %s. Skipping", 113 | cfg.LXDContainerPortKey, container.Name)) 114 | continue 115 | } 116 | 117 | /* iface is the container interface to get an IP address. */ 118 | /* This isn't exposed by the LXD API, and containers can have multiple interfaces, */ 119 | iface := cfg.LXDContainerInterface 120 | if v, ok := container.Config[cfg.LXDContainerInterfaceKey]; ok { 121 | iface = v 122 | } 123 | 124 | ip := "" 125 | if ip, err = lxdDetermineContainerIP(client, container.Name, iface, cfg.LXDContainerAddressType); err != nil { 126 | log.Error(fmt.Sprintf("Can't determine %s container ip address: %s. Skipping", container.Name, err)) 127 | continue 128 | } 129 | 130 | sni := "" 131 | if v, ok := container.Config[cfg.LXDContainerSNIKey]; ok { 132 | sni = v 133 | } 134 | 135 | backends = append(backends, core.Backend{ 136 | Target: core.Target{ 137 | Host: ip, 138 | Port: port, 139 | }, 140 | Priority: 1, 141 | Weight: 1, 142 | Stats: core.BackendStats{ 143 | Live: true, 144 | }, 145 | Sni: sni, 146 | }) 147 | } 148 | 149 | return &backends, nil 150 | } 151 | 152 | /** 153 | * Create new LXD Client 154 | */ 155 | func lxdBuildClient(cfg config.DiscoveryConfig) (lxd.ContainerServer, error) { 156 | log := logging.For("lxdBuildClient") 157 | 158 | /* Make a client to pass around */ 159 | var client lxd.ContainerServer 160 | 161 | /* Build a configuration with the requested options */ 162 | lxdConfig, err := lxdBuildConfig(cfg) 163 | if err != nil { 164 | return client, err 165 | } 166 | 167 | if strings.HasPrefix(cfg.LXDServerAddress, "https:") { 168 | 169 | /* Validate or generate certificates on the client side (gobetween) */ 170 | if cfg.LXDGenerateClientCerts { 171 | log.Debug("Generating LXD client certificates") 172 | if err := lxdConfig.GenerateClientCertificate(); err != nil { 173 | return nil, err 174 | } 175 | } 176 | 177 | /* Validate or accept certificates on the server side (LXD) */ 178 | serverCertf := lxdConfig.ServerCertPath(cfg.LXDServerRemoteName) 179 | if !shared.PathExists(serverCertf) { 180 | /* If the server certificate was not found, either gobetween and the LXD server are set 181 | * up for PKI, or gobetween must authenticate with the LXD server and accept its server 182 | * certificate. 183 | * 184 | * First, see if communication with the LXD server is possible. 185 | */ 186 | _, err := lxdConfig.GetInstanceServer(cfg.LXDServerRemoteName) 187 | if err != nil { 188 | /* If there was an error, then gobetween will try to download the server's cert. */ 189 | if cfg.LXDAcceptServerCert { 190 | log.Debug("Retrieving LXD server certificate") 191 | err := lxdGetRemoteCertificate(lxdConfig, cfg.LXDServerRemoteName) 192 | if err != nil { 193 | return nil, fmt.Errorf("Could obtain LXD server certificate: %s", err) 194 | } 195 | } else { 196 | err := fmt.Errorf("Unable to communicate with LXD server. Either set " + 197 | "lxd_accept_server_cert to true or add the LXD server out of " + 198 | "band of gobetween and try again.") 199 | return nil, err 200 | } 201 | } 202 | } 203 | 204 | /* 205 | * Finally, check and see if gobetween needs to authenticate with the LXD server. 206 | * Authentication happens only once. After that, gobetween will be a trusted client 207 | * as long as the exchanged certificates to not change. 208 | * 209 | * Authentication must happen even if PKI is in use. 210 | */ 211 | client, err = lxdConfig.GetInstanceServer(cfg.LXDServerRemoteName) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | log.Info("Authenticating to LXD server") 217 | err = lxdAuthenticateToServer(client, cfg.LXDServerRemoteName, cfg.LXDServerRemotePassword) 218 | if err != nil { 219 | log.Info("Authentication unsuccessful") 220 | return nil, err 221 | } 222 | 223 | log.Info("Authentication successful") 224 | } 225 | 226 | /* Build a new client */ 227 | client, err = lxdConfig.GetInstanceServer(cfg.LXDServerRemoteName) 228 | if err != nil { 229 | return nil, err 230 | } 231 | 232 | /* Validate the client config and connectivity */ 233 | if _, _, err := client.GetServer(); err != nil { 234 | return nil, err 235 | } 236 | 237 | return client, nil 238 | } 239 | 240 | /** 241 | * Create LXD Client Config 242 | */ 243 | func lxdBuildConfig(cfg config.DiscoveryConfig) (*lxd_config.Config, error) { 244 | log := logging.For("lxdBuildConfig") 245 | 246 | log.Debug("Using API: ", cfg.LXDServerAddress) 247 | 248 | /* Build an LXD configuration that will connect to the requested LXD server */ 249 | var config *lxd_config.Config 250 | if conf, err := lxd_config.LoadConfig(cfg.LXDConfigDirectory); err != nil { 251 | config = &lxd_config.DefaultConfig 252 | config.ConfigDir = cfg.LXDConfigDirectory 253 | } else { 254 | config = conf 255 | } 256 | 257 | config.Remotes[cfg.LXDServerRemoteName] = lxd_config.Remote{Addr: cfg.LXDServerAddress} 258 | return config, nil 259 | } 260 | 261 | /** 262 | * lxdGetRemoteCertificate will attempt to retrieve a remote LXD server's 263 | certificate and save it to the servercert's path. 264 | */ 265 | func lxdGetRemoteCertificate(config *lxd_config.Config, remote string) error { 266 | addr := config.Remotes[remote] 267 | userAgent := "" 268 | certificate, err := shared.GetRemoteCertificate(addr.Addr, userAgent) 269 | if err != nil { 270 | return err 271 | } 272 | 273 | serverCertDir := config.ConfigPath("servercerts") 274 | if err := os.MkdirAll(serverCertDir, 0750); err != nil { 275 | return fmt.Errorf("Could not create server cert dir: %s", err) 276 | } 277 | 278 | certf := fmt.Sprintf("%s/%s.crt", serverCertDir, remote) 279 | certOut, err := os.Create(certf) 280 | if err != nil { 281 | return err 282 | } 283 | 284 | pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw}) 285 | certOut.Close() 286 | 287 | return nil 288 | } 289 | 290 | /** 291 | * lxdAuthenticateToServer authenticates to an LXD Server 292 | */ 293 | func lxdAuthenticateToServer(client lxd.ContainerServer, remote string, password string) error { 294 | srv, _, err := client.GetServer() 295 | if srv.Auth == "trusted" { 296 | return nil 297 | } 298 | 299 | req := lxd_api.CertificatesPost{ 300 | Password: password, 301 | } 302 | req.Type = "client" 303 | 304 | err = client.CreateCertificate(req) 305 | if err != nil { 306 | return fmt.Errorf("Unable to authenticate with remote server: %s", err) 307 | } 308 | 309 | _, _, err = client.GetServer() 310 | if err != nil { 311 | return err 312 | } 313 | 314 | return nil 315 | } 316 | 317 | /** 318 | * Get container IP address depending on network interface and address type 319 | */ 320 | func lxdDetermineContainerIP(client lxd.ContainerServer, container, iface, addrType string) (string, error) { 321 | var containerIP string 322 | 323 | /* Convert addrType to inet */ 324 | var inet string 325 | switch addrType { 326 | case "IPv4": 327 | inet = "inet" 328 | case "IPv6": 329 | inet = "inet6" 330 | } 331 | 332 | cstate, _, err := client.GetContainerState(container) 333 | if err != nil { 334 | return "", err 335 | } 336 | 337 | for i, network := range cstate.Network { 338 | if i != iface { 339 | continue 340 | } 341 | 342 | for _, ip := range network.Addresses { 343 | if ip.Family == inet { 344 | containerIP = ip.Address 345 | break 346 | } 347 | } 348 | } 349 | 350 | /* If IPv6, format correctly */ 351 | if inet == "inet6" { 352 | containerIP = fmt.Sprintf("[%s]", containerIP) 353 | } 354 | 355 | if containerIP == "" { 356 | return "", fmt.Errorf("Unable to determine IP address for LXD container %s", container) 357 | } 358 | 359 | return containerIP, nil 360 | } 361 | -------------------------------------------------------------------------------- /src/discovery/plaintext.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | /** 4 | * plaintext.go - Plaintext discovery implementation 5 | * 6 | * @author Ievgen Ponomarenko 7 | * @author Yaroslav Pogrebnyak 8 | */ 9 | 10 | import ( 11 | "io/ioutil" 12 | "net/http" 13 | "strings" 14 | "time" 15 | 16 | "github.com/yyyar/gobetween/config" 17 | "github.com/yyyar/gobetween/core" 18 | "github.com/yyyar/gobetween/logging" 19 | "github.com/yyyar/gobetween/utils" 20 | "github.com/yyyar/gobetween/utils/parsers" 21 | ) 22 | 23 | const ( 24 | plaintextDefaultRetryWaitDuration = 2 * time.Second 25 | plaintextDefaultHttpTimeout = 5 * time.Second 26 | ) 27 | 28 | /** 29 | * Create new Discovery with Plaintext fetch func 30 | */ 31 | func NewPlaintextDiscovery(cfg config.DiscoveryConfig) interface{} { 32 | 33 | if cfg.PlaintextRegexpPattern == "" { 34 | cfg.PlaintextRegexpPattern = parsers.DEFAULT_BACKEND_PATTERN 35 | } 36 | 37 | d := Discovery{ 38 | opts: DiscoveryOpts{plaintextDefaultRetryWaitDuration}, 39 | fetch: plaintextFetch, 40 | cfg: cfg, 41 | } 42 | 43 | return &d 44 | } 45 | 46 | /** 47 | * Fetch / refresh backends from URL with plain text 48 | */ 49 | func plaintextFetch(cfg config.DiscoveryConfig) (*[]core.Backend, error) { 50 | 51 | log := logging.For("plaintextFetch") 52 | 53 | log.Info("Fetching ", cfg.PlaintextEndpoint) 54 | 55 | // Make request 56 | timeout := utils.ParseDurationOrDefault(cfg.Timeout, plaintextDefaultHttpTimeout) 57 | client := http.Client{Timeout: timeout} 58 | res, err := client.Get(cfg.PlaintextEndpoint) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | defer res.Body.Close() 64 | 65 | // Read response 66 | content, err := ioutil.ReadAll(res.Body) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | backends := []core.Backend{} 72 | lines := strings.Split(string(content), "\n") 73 | 74 | // Iterate and parse 75 | for _, line := range lines { 76 | 77 | if line == "" { 78 | continue 79 | } 80 | 81 | backend, err := parsers.ParseBackend(line, cfg.PlaintextRegexpPattern) 82 | if err != nil { 83 | log.Warn("Cant parse ", line, err) 84 | continue 85 | } 86 | 87 | backends = append(backends, *backend) 88 | } 89 | 90 | return &backends, err 91 | } 92 | -------------------------------------------------------------------------------- /src/discovery/srv.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | /** 4 | * srv.go - SRV record DNS resolve discovery implementation 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "strings" 13 | "time" 14 | 15 | "github.com/miekg/dns" 16 | "github.com/yyyar/gobetween/config" 17 | "github.com/yyyar/gobetween/core" 18 | "github.com/yyyar/gobetween/logging" 19 | "github.com/yyyar/gobetween/utils" 20 | ) 21 | 22 | const ( 23 | srvRetryWaitDuration = 2 * time.Second 24 | srvDefaultWaitTimeout = 5 * time.Second 25 | srvUdpSize = 4096 26 | ) 27 | 28 | func NewSrvDiscovery(cfg config.DiscoveryConfig) interface{} { 29 | 30 | d := Discovery{ 31 | opts: DiscoveryOpts{srvRetryWaitDuration}, 32 | fetch: srvFetch, 33 | cfg: cfg, 34 | } 35 | 36 | return &d 37 | } 38 | 39 | /** 40 | * Create new Discovery with Srv fetch func 41 | */ 42 | func srvFetch(cfg config.DiscoveryConfig) (*[]core.Backend, error) { 43 | 44 | log := logging.For("srvFetch") 45 | 46 | log.Info("Fetching ", cfg.SrvLookupServer, " ", cfg.SrvLookupPattern) 47 | 48 | /* ----- perform query srv ----- */ 49 | 50 | r, err := srvDnsLookup(cfg, cfg.SrvLookupPattern, dns.TypeSRV) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | if len(r.Answer) == 0 { 56 | log.Warn("Empty response from", cfg.SrvLookupServer, cfg.SrvLookupPattern) 57 | return &[]core.Backend{}, nil 58 | } 59 | 60 | /* ----- try to get IPs from additional section ------ */ 61 | 62 | hosts := make(map[string]string) // name -> host 63 | for _, ans := range r.Extra { 64 | switch record := ans.(type) { 65 | case *dns.A: 66 | hosts[record.Header().Name] = record.A.String() 67 | case *dns.AAAA: 68 | hosts[record.Header().Name] = fmt.Sprintf("[%s]", record.AAAA.String()) 69 | } 70 | } 71 | 72 | /* ----- create backend list looking up IP if needed ----- */ 73 | 74 | backends := []core.Backend{} 75 | for _, ans := range r.Answer { 76 | record, ok := ans.(*dns.SRV) 77 | if !ok { 78 | return nil, errors.New("Non-SRV record in SRV answer") 79 | } 80 | 81 | // If there were no A/AAAA record in additional SRV response, 82 | // fetch it 83 | if _, ok := hosts[record.Target]; !ok { 84 | log.Debug("Fetching ", cfg.SrvLookupServer, " A/AAAA ", record.Target) 85 | 86 | ip, err := srvIPLookup(cfg, record.Target, dns.TypeA) 87 | if err != nil { 88 | log.Warn("Error fetching A record for ", record.Target, ": ", err) 89 | } 90 | 91 | if ip == "" { 92 | ip, err = srvIPLookup(cfg, record.Target, dns.TypeAAAA) 93 | if err != nil { 94 | log.Warn("Error fetching AAAA record for ", record.Target, ": ", err) 95 | } 96 | } 97 | 98 | if ip != "" { 99 | hosts[record.Target] = ip 100 | } else { 101 | log.Warn("No IP found for ", record.Target, ", skipping...") 102 | continue 103 | } 104 | } 105 | 106 | // Append new backends 107 | backends = append(backends, core.Backend{ 108 | Target: core.Target{ 109 | Host: hosts[record.Target], 110 | Port: fmt.Sprintf("%v", record.Port), 111 | }, 112 | Priority: int(record.Priority), 113 | Weight: int(record.Weight), 114 | Stats: core.BackendStats{ 115 | Live: true, 116 | }, 117 | Sni: strings.TrimRight(record.Target, "."), 118 | }) 119 | } 120 | 121 | return &backends, nil 122 | } 123 | 124 | /** 125 | * Perform DNS Lookup with needed pattern and type 126 | */ 127 | func srvDnsLookup(cfg config.DiscoveryConfig, pattern string, typ uint16) (*dns.Msg, error) { 128 | timeout := utils.ParseDurationOrDefault(cfg.Timeout, srvDefaultWaitTimeout) 129 | c := dns.Client{Net: cfg.SrvDnsProtocol, Timeout: timeout} 130 | m := dns.Msg{} 131 | 132 | m.SetQuestion(pattern, typ) 133 | m.SetEdns0(srvUdpSize, true) 134 | r, _, err := c.Exchange(&m, cfg.SrvLookupServer) 135 | 136 | return r, err 137 | } 138 | 139 | /** 140 | * Perform DNS lookup and extract IP address 141 | */ 142 | func srvIPLookup(cfg config.DiscoveryConfig, pattern string, typ uint16) (string, error) { 143 | resp, err := srvDnsLookup(cfg, pattern, typ) 144 | if err != nil { 145 | return "", err 146 | } 147 | 148 | if len(resp.Answer) == 0 { 149 | return "", nil 150 | } 151 | 152 | switch ans := resp.Answer[0].(type) { 153 | case *dns.A: 154 | return ans.A.String(), nil 155 | case *dns.AAAA: 156 | return fmt.Sprintf("[%s]", ans.AAAA.String()), nil 157 | default: 158 | return "", nil 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/discovery/static.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | /** 4 | * static.go - static list discovery implementation 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "github.com/yyyar/gobetween/config" 11 | "github.com/yyyar/gobetween/core" 12 | "github.com/yyyar/gobetween/logging" 13 | "github.com/yyyar/gobetween/utils/parsers" 14 | ) 15 | 16 | /** 17 | * Creates new static discovery 18 | */ 19 | func NewStaticDiscovery(cfg config.DiscoveryConfig) interface{} { 20 | 21 | d := Discovery{ 22 | opts: DiscoveryOpts{0}, 23 | cfg: cfg, 24 | fetch: staticFetch, 25 | } 26 | 27 | return &d 28 | } 29 | 30 | /** 31 | * Start discovery 32 | */ 33 | func staticFetch(cfg config.DiscoveryConfig) (*[]core.Backend, error) { 34 | 35 | log := logging.For("discovery/static") 36 | 37 | var backends []core.Backend 38 | for _, s := range cfg.StaticList { 39 | backend, err := parsers.ParseBackendDefault(s) 40 | if err != nil { 41 | log.Warn(err) 42 | continue 43 | } 44 | backends = append(backends, *backend) 45 | } 46 | 47 | return &backends, nil 48 | } 49 | -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yyyar/gobetween 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/burntsushi/toml v0.3.1 7 | github.com/elgs/gojq v0.0.0-20230628214826-df5c4045598e 8 | github.com/eric-lindau/udpfacade v0.0.0-20190621043444-d8c1c27add16 9 | github.com/fsouza/go-dockerclient v1.12.1 10 | github.com/gin-contrib/cors v1.7.3 11 | github.com/gin-gonic/gin v1.10.0 12 | github.com/hashicorp/consul/api v1.31.1 13 | github.com/lxc/lxd v0.0.0-20200706202337-814c96fcec74 14 | github.com/miekg/dns v1.1.63 15 | github.com/pires/go-proxyproto v0.8.0 16 | github.com/prometheus/client_golang v1.20.5 17 | github.com/sirupsen/logrus v1.9.3 18 | github.com/spf13/cobra v1.9.1 19 | golang.org/x/crypto v0.37.0 20 | ) 21 | 22 | require ( 23 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect 24 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 25 | github.com/Microsoft/go-winio v0.6.2 // indirect 26 | github.com/armon/go-metrics v0.4.1 // indirect 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/bytedance/sonic v1.12.8 // indirect 29 | github.com/bytedance/sonic/loader v0.2.3 // indirect 30 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 31 | github.com/cloudwego/base64x v0.1.5 // indirect 32 | github.com/containerd/log v0.1.0 // indirect 33 | github.com/docker/docker v28.1.1+incompatible // indirect 34 | github.com/docker/go-connections v0.5.0 // indirect 35 | github.com/docker/go-units v0.5.0 // indirect 36 | github.com/elgs/gosplitargs v0.0.0-20241205072753-cbd889c0f906 // indirect 37 | github.com/fatih/color v1.18.0 // indirect 38 | github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect 39 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 40 | github.com/gin-contrib/sse v1.0.0 // indirect 41 | github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect 42 | github.com/go-playground/locales v0.14.1 // indirect 43 | github.com/go-playground/universal-translator v0.18.1 // indirect 44 | github.com/go-playground/validator/v10 v10.25.0 // indirect 45 | github.com/goccy/go-json v0.10.5 // indirect 46 | github.com/gogo/protobuf v1.3.2 // indirect 47 | github.com/golang/protobuf v1.5.4 // indirect 48 | github.com/google/gopacket v1.1.19 // indirect 49 | github.com/gorilla/websocket v1.5.3 // indirect 50 | github.com/hashicorp/errwrap v1.1.0 // indirect 51 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 52 | github.com/hashicorp/go-hclog v1.6.3 // indirect 53 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 54 | github.com/hashicorp/go-metrics v0.5.4 // indirect 55 | github.com/hashicorp/go-multierror v1.1.1 // indirect 56 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 57 | github.com/hashicorp/golang-lru v1.0.2 // indirect 58 | github.com/hashicorp/serf v0.10.2 // indirect 59 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 60 | github.com/json-iterator/go v1.1.12 // indirect 61 | github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a // indirect 62 | github.com/juju/persistent-cookiejar v1.0.0 // indirect 63 | github.com/juju/schema v1.2.0 // indirect 64 | github.com/juju/webbrowser v1.0.0 // indirect 65 | github.com/julienschmidt/httprouter v1.3.0 // indirect 66 | github.com/klauspost/compress v1.18.0 // indirect 67 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 68 | github.com/leodido/go-urn v1.4.0 // indirect 69 | github.com/mattn/go-colorable v0.1.14 // indirect 70 | github.com/mattn/go-isatty v0.0.20 // indirect 71 | github.com/mitchellh/go-homedir v1.1.0 // indirect 72 | github.com/mitchellh/mapstructure v1.5.0 // indirect 73 | github.com/moby/docker-image-spec v1.3.1 // indirect 74 | github.com/moby/go-archive v0.1.0 // indirect 75 | github.com/moby/patternmatcher v0.6.0 // indirect 76 | github.com/moby/sys/sequential v0.6.0 // indirect 77 | github.com/moby/sys/user v0.4.0 // indirect 78 | github.com/moby/sys/userns v0.1.0 // indirect 79 | github.com/moby/term v0.5.2 // indirect 80 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 81 | github.com/modern-go/reflect2 v1.0.2 // indirect 82 | github.com/morikuni/aec v1.0.0 // indirect 83 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 84 | github.com/opencontainers/go-digest v1.0.0 // indirect 85 | github.com/opencontainers/image-spec v1.1.1 // indirect 86 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 87 | github.com/pkg/errors v0.9.1 // indirect 88 | github.com/prometheus/client_model v0.6.1 // indirect 89 | github.com/prometheus/common v0.62.0 // indirect 90 | github.com/prometheus/procfs v0.15.1 // indirect 91 | github.com/rogpeppe/fastuuid v1.2.0 // indirect 92 | github.com/spf13/pflag v1.0.6 // indirect 93 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 94 | github.com/ugorji/go/codec v1.2.12 // indirect 95 | golang.org/x/arch v0.14.0 // indirect 96 | golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect 97 | golang.org/x/mod v0.23.0 // indirect 98 | golang.org/x/net v0.35.0 // indirect 99 | golang.org/x/sync v0.13.0 // indirect 100 | golang.org/x/sys v0.32.0 // indirect 101 | golang.org/x/term v0.31.0 // indirect 102 | golang.org/x/text v0.24.0 // indirect 103 | golang.org/x/tools v0.30.0 // indirect 104 | google.golang.org/protobuf v1.36.5 // indirect 105 | gopkg.in/errgo.v1 v1.0.1 // indirect 106 | gopkg.in/httprequest.v1 v1.2.1 // indirect 107 | gopkg.in/juju/environschema.v1 v1.0.1 // indirect 108 | gopkg.in/macaroon-bakery.v2 v2.3.0 // indirect 109 | gopkg.in/macaroon.v2 v2.1.0 // indirect 110 | gopkg.in/retry.v1 v1.0.3 // indirect 111 | gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5 // indirect 112 | gopkg.in/yaml.v2 v2.4.0 // indirect 113 | gopkg.in/yaml.v3 v3.0.1 // indirect 114 | ) 115 | -------------------------------------------------------------------------------- /src/healthcheck/exec.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | /** 4 | * exec.go - Exec healthcheck 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/yyyar/gobetween/config" 13 | "github.com/yyyar/gobetween/core" 14 | "github.com/yyyar/gobetween/logging" 15 | "github.com/yyyar/gobetween/utils" 16 | ) 17 | 18 | /** 19 | * Exec healthcheck 20 | */ 21 | func exec(t core.Target, cfg config.HealthcheckConfig, result chan<- CheckResult) { 22 | 23 | log := logging.For("healthcheck/exec") 24 | 25 | execTimeout, _ := time.ParseDuration(cfg.Timeout) 26 | 27 | checkResult := CheckResult{ 28 | Target: t, 29 | } 30 | 31 | out, err := utils.ExecTimeout(execTimeout, cfg.ExecCommand, t.Host, t.Port) 32 | if err != nil { 33 | // TODO: Decide better what to do in this case 34 | checkResult.Status = Unhealthy 35 | log.Warn(err) 36 | } else { 37 | if out == cfg.ExecExpectedPositiveOutput { 38 | checkResult.Status = Healthy 39 | } else if out == cfg.ExecExpectedNegativeOutput { 40 | checkResult.Status = Unhealthy 41 | } else { 42 | log.Warn("Unexpected output: ", out) 43 | } 44 | } 45 | 46 | select { 47 | case result <- checkResult: 48 | default: 49 | log.Warn("Channel is full. Discarding value") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/healthcheck/healthcheck.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | /** 4 | * healthcheck.go - Healtheck 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "github.com/yyyar/gobetween/config" 11 | "github.com/yyyar/gobetween/core" 12 | ) 13 | 14 | /** 15 | * Health Check function 16 | * Returns channel in which only one check result will be delivered 17 | */ 18 | type CheckFunc func(core.Target, config.HealthcheckConfig, chan<- CheckResult) 19 | 20 | type HealthCheckStatus int32 21 | 22 | const ( 23 | Initial HealthCheckStatus = iota 24 | Unhealthy 25 | Healthy 26 | ) 27 | 28 | /** 29 | * Check result 30 | * Handles target and it's live status 31 | */ 32 | type CheckResult struct { 33 | 34 | /* Check target */ 35 | Target core.Target 36 | 37 | /* Check live status */ 38 | Status HealthCheckStatus 39 | } 40 | 41 | /** 42 | * Healthcheck 43 | */ 44 | type Healthcheck struct { 45 | 46 | /* Healthcheck function */ 47 | check CheckFunc 48 | 49 | /* Healthcheck configuration */ 50 | cfg config.HealthcheckConfig 51 | 52 | /* Input channel to accept targets */ 53 | In chan []core.Target 54 | 55 | /* Output channel to send check results for individual target */ 56 | Out chan CheckResult 57 | 58 | /* Current check workers */ 59 | workers []*Worker 60 | 61 | /* Channel to handle stop */ 62 | stop chan bool 63 | } 64 | 65 | /** 66 | * Registry of factory methods 67 | */ 68 | var registry = make(map[string]CheckFunc) 69 | 70 | /** 71 | * Initialize type registry 72 | */ 73 | func init() { 74 | registry["ping"] = ping 75 | registry["probe"] = probe 76 | registry["exec"] = exec 77 | registry["none"] = nil 78 | } 79 | 80 | /** 81 | * Create new Discovery based on strategy 82 | */ 83 | func New(strategy string, cfg config.HealthcheckConfig) *Healthcheck { 84 | 85 | check := registry[strategy] 86 | 87 | /* Create healthcheck */ 88 | 89 | h := Healthcheck{ 90 | check: check, 91 | cfg: cfg, 92 | In: make(chan []core.Target), 93 | Out: make(chan CheckResult), 94 | workers: []*Worker{}, 95 | stop: make(chan bool), 96 | } 97 | 98 | return &h 99 | } 100 | 101 | /** 102 | * Start healthcheck 103 | */ 104 | func (this *Healthcheck) Start() { 105 | 106 | go func() { 107 | for { 108 | select { 109 | 110 | /* got new targets */ 111 | case targets := <-this.In: 112 | this.UpdateWorkers(targets) 113 | 114 | /* got stop requst */ 115 | case <-this.stop: 116 | 117 | // Stop all workers 118 | for i := range this.workers { 119 | this.workers[i].Stop() 120 | } 121 | 122 | // And free it's memory 123 | this.workers = []*Worker{} 124 | 125 | return 126 | } 127 | } 128 | }() 129 | } 130 | 131 | /** 132 | * Sync current workers to represent healtcheck on targets 133 | * Will remove not needed workers, and add needed 134 | */ 135 | func (this *Healthcheck) UpdateWorkers(targets []core.Target) { 136 | 137 | result := []*Worker{} 138 | 139 | // Keep or add needed workers 140 | for _, t := range targets { 141 | var keep *Worker 142 | for i := range this.workers { 143 | c := this.workers[i] 144 | if t.EqualTo(c.target) { 145 | keep = c 146 | break 147 | } 148 | } 149 | 150 | if keep == nil { 151 | keep = &Worker{ 152 | target: t, 153 | stop: make(chan bool), 154 | out: this.Out, 155 | cfg: this.cfg, 156 | check: this.check, 157 | LastResult: CheckResult{ 158 | Status: Initial, 159 | }, 160 | } 161 | keep.Start() 162 | } 163 | result = append(result, keep) 164 | } 165 | 166 | // Stop needed workers 167 | for i := range this.workers { 168 | c := this.workers[i] 169 | remove := true 170 | for _, t := range targets { 171 | if c.target.EqualTo(t) { 172 | remove = false 173 | break 174 | } 175 | } 176 | 177 | if remove { 178 | c.Stop() 179 | } 180 | } 181 | 182 | this.workers = result 183 | 184 | } 185 | 186 | func (this *Healthcheck) HasCheck() bool { 187 | return this.cfg.Kind != "none" 188 | } 189 | 190 | func (this *Healthcheck) InitialBackendHealthCheckStatus() HealthCheckStatus { 191 | if !this.HasCheck() { 192 | return Healthy 193 | } 194 | if this.cfg.InitialStatus != nil { 195 | switch *this.cfg.InitialStatus { 196 | case "unhealthy": 197 | return Unhealthy 198 | case "healthy": 199 | return Healthy 200 | default: 201 | panic("Healthcheck invalid initial status, this should have been validated in manager, but has invalid value " + *this.cfg.InitialStatus) 202 | } 203 | } 204 | return Healthy 205 | } 206 | 207 | /** 208 | * Stop healthcheck 209 | */ 210 | func (this *Healthcheck) Stop() { 211 | this.stop <- true 212 | } 213 | -------------------------------------------------------------------------------- /src/healthcheck/ping.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | /** 4 | * ping.go - TCP ping healthcheck 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "net" 11 | "time" 12 | 13 | "github.com/yyyar/gobetween/config" 14 | "github.com/yyyar/gobetween/core" 15 | "github.com/yyyar/gobetween/logging" 16 | ) 17 | 18 | /** 19 | * Ping healthcheck 20 | */ 21 | func ping(t core.Target, cfg config.HealthcheckConfig, result chan<- CheckResult) { 22 | 23 | pingTimeoutDuration, _ := time.ParseDuration(cfg.Timeout) 24 | 25 | log := logging.For("healthcheck/ping") 26 | 27 | checkResult := CheckResult{ 28 | Target: t, 29 | } 30 | 31 | conn, err := net.DialTimeout("tcp", t.Address(), pingTimeoutDuration) 32 | if err != nil { 33 | checkResult.Status = Unhealthy 34 | } else { 35 | checkResult.Status = Healthy 36 | conn.Close() 37 | } 38 | 39 | select { 40 | case result <- checkResult: 41 | default: 42 | log.Warn("Channel is full. Discarding value") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/healthcheck/probe.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | /** 4 | * probe.go - TCP/UDP and TLS probe healthcheck 5 | * 6 | * @author Yousong Zhou 7 | * @author Illarion Kovalchuk 8 | */ 9 | 10 | import ( 11 | "bytes" 12 | "crypto/tls" 13 | "io" 14 | "net" 15 | "regexp" 16 | "time" 17 | 18 | "github.com/yyyar/gobetween/config" 19 | "github.com/yyyar/gobetween/core" 20 | "github.com/yyyar/gobetween/logging" 21 | ) 22 | 23 | func probe(t core.Target, cfg config.HealthcheckConfig, result chan<- CheckResult) { 24 | log := logging.For("healthcheck/probe") 25 | 26 | timeout, _ := time.ParseDuration(cfg.Timeout) 27 | 28 | checkResult := CheckResult{ 29 | Status: Unhealthy, 30 | Target: t, 31 | } 32 | 33 | defer func() { 34 | select { 35 | case result <- checkResult: 36 | default: 37 | log.Warn("Channel is full. Discarding value") 38 | } 39 | }() 40 | 41 | var conn net.Conn 42 | var err error 43 | 44 | switch cfg.ProbeProtocol { 45 | case "tls": 46 | conn, err = tls.DialWithDialer(&net.Dialer{ 47 | Timeout: timeout, 48 | }, "tcp", t.Address(), &tls.Config{}) 49 | default: 50 | conn, err = net.DialTimeout(cfg.ProbeProtocol, t.Address(), timeout) 51 | } 52 | if err != nil { 53 | checkResult.Status = Unhealthy 54 | return 55 | } 56 | 57 | defer conn.Close() 58 | 59 | send := []byte(cfg.ProbeSend) 60 | 61 | recv := []byte(cfg.ProbeRecv) 62 | recvLen := cfg.ProbeRecvLen 63 | 64 | if recvLen == 0 { 65 | recvLen = len(recv) 66 | } 67 | 68 | if timeout > 0 { 69 | err = conn.SetWriteDeadline(time.Now().Add(timeout)) 70 | if err != nil { 71 | log.Errorf("Could not set write timeout: %v", err) 72 | return 73 | } 74 | } 75 | 76 | n, err := conn.Write(send) 77 | if err != nil { 78 | log.Debugf("Could not send probe: %v", err) 79 | return 80 | } 81 | 82 | if n != len(send) { 83 | log.Debugf("Incomplete probe write") 84 | return 85 | } 86 | 87 | if timeout > 0 { 88 | err = conn.SetReadDeadline(time.Now().Add(timeout)) 89 | if err != nil { 90 | log.Errorf("Could not set read timeout: %v", err) 91 | return 92 | } 93 | } 94 | 95 | actual := make([]byte, recvLen) 96 | n, err = io.ReadFull(conn, actual) 97 | if err != nil { 98 | log.Debugf("Could not read from backend: %v", err) 99 | return 100 | } 101 | 102 | switch cfg.ProbeStrategy { 103 | case "starts_with": 104 | if !bytes.Equal(actual, recv) { 105 | log.Debugf("Bytes received from backend:\n% x\nbytes expected:\n% x", actual, recv) 106 | return 107 | } 108 | case "regexp": 109 | re := regexp.MustCompile(cfg.ProbeRecv) 110 | if !re.Match(actual) { 111 | log.Debugf("Bytes received from backend: % x did not match %v", actual, cfg.ProbeRecv) 112 | return 113 | } 114 | default: 115 | panic("probe_strategy should be checked in manager") 116 | } 117 | 118 | checkResult.Status = Healthy 119 | } 120 | -------------------------------------------------------------------------------- /src/healthcheck/worker.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | /** 4 | * worker.go - Healtheck worker 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/yyyar/gobetween/config" 13 | "github.com/yyyar/gobetween/core" 14 | "github.com/yyyar/gobetween/logging" 15 | ) 16 | 17 | /** 18 | * Healthcheck Worker 19 | * Handles all periodic healthcheck logic 20 | * and yields results on change 21 | */ 22 | type Worker struct { 23 | 24 | /* Target to monitor and check */ 25 | target core.Target 26 | 27 | /* Function that does actual check */ 28 | check CheckFunc 29 | 30 | /* Channel to write changed check results */ 31 | out chan<- CheckResult 32 | 33 | /* Healthcheck configuration */ 34 | cfg config.HealthcheckConfig 35 | 36 | /* Stop channel to worker to stop */ 37 | stop chan bool 38 | 39 | /* Last confirmed check result */ 40 | LastResult CheckResult 41 | 42 | /* Current passes count, if LastResult.Live = true */ 43 | passes int 44 | 45 | /* Current fails count, if LastResult.Live = false */ 46 | fails int 47 | } 48 | 49 | /** 50 | * Start worker 51 | */ 52 | func (this *Worker) Start() { 53 | 54 | log := logging.For("healthcheck/worker") 55 | 56 | // Special case for no healthcheck, don't actually start worker 57 | if this.cfg.Kind == "none" { 58 | return 59 | } 60 | 61 | interval, _ := time.ParseDuration(this.cfg.Interval) 62 | 63 | ticker := time.NewTicker(interval) 64 | c := make(chan CheckResult, 1) 65 | 66 | go func() { 67 | /* Check health before any delay*/ 68 | log.Debug("Initial check ", this.cfg.Kind, " for ", this.target) 69 | go this.check(this.target, this.cfg, c) 70 | for { 71 | select { 72 | 73 | /* new check interval has reached */ 74 | case <-ticker.C: 75 | log.Debug("Next check ", this.cfg.Kind, " for ", this.target) 76 | go this.check(this.target, this.cfg, c) 77 | 78 | /* new check result is ready */ 79 | case checkResult := <-c: 80 | log.Debug("Got check result ", this.cfg.Kind, ": ", checkResult) 81 | this.process(checkResult) 82 | 83 | /* request to stop worker */ 84 | case <-this.stop: 85 | ticker.Stop() 86 | //close(c) // TODO: Check! 87 | return 88 | } 89 | } 90 | }() 91 | } 92 | 93 | /** 94 | * Process next check result, 95 | * counting passes and fails as needed, and 96 | * sending updated check result to out 97 | */ 98 | func (this *Worker) process(checkResult CheckResult) { 99 | 100 | log := logging.For("healthcheck/worker") 101 | 102 | if checkResult.Status == this.LastResult.Status { 103 | // check status not changed 104 | return 105 | } 106 | 107 | if checkResult.Status == Unhealthy { 108 | this.passes = 0 109 | this.fails++ 110 | } else if checkResult.Status == Healthy { 111 | this.fails = 0 112 | this.passes++ 113 | } 114 | 115 | if this.passes == 0 && this.fails >= this.cfg.Fails || 116 | this.fails == 0 && this.passes >= this.cfg.Passes { 117 | this.LastResult = checkResult 118 | 119 | log.Info("Sending to scheduler: ", this.LastResult) 120 | this.out <- checkResult 121 | } 122 | } 123 | 124 | /** 125 | * Stop worker 126 | */ 127 | func (this *Worker) Stop() { 128 | close(this.stop) 129 | } 130 | -------------------------------------------------------------------------------- /src/info/info.go: -------------------------------------------------------------------------------- 1 | package info 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var ( 8 | Version string 9 | Revision string 10 | Branch string 11 | StartTime time.Time 12 | Configuration interface{} 13 | ) 14 | -------------------------------------------------------------------------------- /src/logging/log.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | /** 4 | * log.go - logging wrapper 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "os" 13 | "strings" 14 | 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | /** 19 | * Logging initialize 20 | */ 21 | func init() { 22 | logrus.SetFormatter(new(MyFormatter)) 23 | logrus.SetLevel(logrus.InfoLevel) 24 | logrus.SetOutput(os.Stdout) 25 | } 26 | 27 | /** 28 | * Configure logging 29 | */ 30 | func Configure(output string, l string, format string) { 31 | 32 | if output == "" || output == "stdout" { 33 | logrus.SetOutput(os.Stdout) 34 | } else if output == "stderr" { 35 | logrus.SetOutput(os.Stderr) 36 | } else { 37 | f, err := os.OpenFile(output, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0755) 38 | if err != nil { 39 | logrus.Fatal(err) 40 | } 41 | logrus.SetOutput(f) 42 | } 43 | 44 | if format == "json" { 45 | logrus.SetFormatter(&logrus.JSONFormatter{}) 46 | } 47 | 48 | if l == "" { 49 | return 50 | } 51 | 52 | if level, err := logrus.ParseLevel(l); err != nil { 53 | logrus.Fatal("Unknown loglevel ", l) 54 | } else { 55 | logrus.SetLevel(level) 56 | } 57 | } 58 | 59 | /** 60 | * Our custom formatter 61 | */ 62 | type MyFormatter struct{} 63 | 64 | /** 65 | * Format entry 66 | */ 67 | func (f *MyFormatter) Format(entry *logrus.Entry) ([]byte, error) { 68 | b := &bytes.Buffer{} 69 | name, ok := entry.Data["name"] 70 | if !ok { 71 | name = "default" 72 | } 73 | fmt.Fprintf(b, "%s [%-5.5s] (%s): %s\n", entry.Time.Format("2006-01-02 15:04:05"), strings.ToUpper(entry.Level.String()), name, entry.Message) 74 | return b.Bytes(), nil 75 | } 76 | 77 | /** 78 | * Add logger name as field var 79 | */ 80 | func For(name string) *logrus.Entry { 81 | return logrus.WithField("name", name) 82 | } 83 | 84 | /* ----- Wrap logrus ------ */ 85 | 86 | func Debug(args ...interface{}) { 87 | logrus.Debug(args...) 88 | } 89 | 90 | func Info(args ...interface{}) { 91 | logrus.Info(args...) 92 | } 93 | 94 | func Warn(args ...interface{}) { 95 | logrus.Warn(args...) 96 | } 97 | 98 | func Error(args ...interface{}) { 99 | logrus.Error(args...) 100 | } 101 | 102 | func Fatal(args ...interface{}) { 103 | logrus.Fatal(args...) 104 | } 105 | -------------------------------------------------------------------------------- /src/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "runtime" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/prometheus/client_golang/prometheus/promhttp" 10 | "github.com/yyyar/gobetween/config" 11 | "github.com/yyyar/gobetween/core" 12 | "github.com/yyyar/gobetween/info" 13 | "github.com/yyyar/gobetween/logging" 14 | "github.com/yyyar/gobetween/stats/counters" 15 | ) 16 | 17 | const ( 18 | namespace = "gobetween" 19 | ) 20 | 21 | var ( 22 | metricsDisabled bool = false 23 | log = logging.For("metrics") 24 | 25 | buildInfo *prometheus.GaugeVec 26 | version string 27 | revision string 28 | branch string 29 | 30 | serverCount *prometheus.GaugeVec 31 | serverActiveConnections *prometheus.GaugeVec 32 | serverRxTotal *prometheus.GaugeVec 33 | serverTxTotal *prometheus.GaugeVec 34 | serverRxSecond *prometheus.GaugeVec 35 | serverTxSecond *prometheus.GaugeVec 36 | 37 | backendActiveConnections *prometheus.GaugeVec 38 | backendRefusedConnections *prometheus.GaugeVec 39 | backendTotalConnections *prometheus.GaugeVec 40 | backendRxBytes *prometheus.GaugeVec 41 | backendTxBytes *prometheus.GaugeVec 42 | backendRxSecond *prometheus.GaugeVec 43 | backendTxSecond *prometheus.GaugeVec 44 | backendLive *prometheus.GaugeVec 45 | ) 46 | 47 | func defineMetrics() { 48 | 49 | buildInfo = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 50 | Namespace: namespace, 51 | Name: "build_info", 52 | Help: fmt.Sprintf( 53 | "A metric with a constant '1' value labeled by version, revision, branch, and goversion from which %s was built.", 54 | namespace, 55 | ), 56 | }, []string{"version", "revision", "branch", "goversion"}) 57 | 58 | serverCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 59 | Namespace: namespace, 60 | Subsystem: "server", 61 | Name: "count", 62 | Help: "Server Count.", 63 | }, []string{"server"}) 64 | 65 | serverActiveConnections = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 66 | Namespace: namespace, 67 | Subsystem: "server", 68 | Name: "active_connections", 69 | Help: "Server Actice Connections.", 70 | }, []string{"server"}) 71 | 72 | serverRxTotal = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 73 | Namespace: namespace, 74 | Subsystem: "server", 75 | Name: "rx_total", 76 | Help: "Server Rx Total.", 77 | }, []string{"server"}) 78 | 79 | serverTxTotal = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 80 | Namespace: namespace, 81 | Subsystem: "server", 82 | Name: "tx_total", 83 | Help: "Server Tx Total.", 84 | }, []string{"server"}) 85 | 86 | serverRxSecond = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 87 | Namespace: namespace, 88 | Subsystem: "server", 89 | Name: "rx_second", 90 | Help: "Server Rx per Second.", 91 | }, []string{"server"}) 92 | 93 | serverTxSecond = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 94 | Namespace: namespace, 95 | Subsystem: "server", 96 | Name: "tx_second", 97 | Help: "Server Tx per Second.", 98 | }, []string{"server"}) 99 | 100 | backendActiveConnections = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 101 | Namespace: namespace, 102 | Subsystem: "backend", 103 | Name: "active_connections", 104 | Help: "Backend Actice Connections.", 105 | }, []string{"server", "host", "port"}) 106 | 107 | backendRefusedConnections = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 108 | Namespace: namespace, 109 | Subsystem: "backend", 110 | Name: "refused_connections", 111 | Help: "Backend Refused Connections.", 112 | }, []string{"server", "host", "port"}) 113 | 114 | backendTotalConnections = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 115 | Namespace: namespace, 116 | Subsystem: "backend", 117 | Name: "total_connections", 118 | Help: "Backend Total Connections.", 119 | }, []string{"server", "host", "port"}) 120 | 121 | backendRxBytes = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 122 | Namespace: namespace, 123 | Subsystem: "backend", 124 | Name: "rx_bytes", 125 | Help: "Backend Rx Bytes.", 126 | }, []string{"server", "host", "port"}) 127 | 128 | backendTxBytes = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 129 | Namespace: namespace, 130 | Subsystem: "backend", 131 | Name: "tx_bytes", 132 | Help: "Backend Tx Bytes.", 133 | }, []string{"server", "host", "port"}) 134 | 135 | backendRxSecond = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 136 | Namespace: namespace, 137 | Subsystem: "backend", 138 | Name: "rx_second", 139 | Help: "Backend Rx per Second.", 140 | }, []string{"server", "host", "port"}) 141 | 142 | backendTxSecond = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 143 | Namespace: namespace, 144 | Subsystem: "backend", 145 | Name: "tx_second", 146 | Help: "Backend Tx per Second.", 147 | }, []string{"server", "host", "port"}) 148 | 149 | backendLive = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 150 | Namespace: namespace, 151 | Subsystem: "backend", 152 | Name: "live", 153 | Help: "Backend Alive.", 154 | }, []string{"server", "host", "port"}) 155 | 156 | } 157 | 158 | func Start(cfg config.MetricsConfig) { 159 | 160 | if !cfg.Enabled { 161 | log.Info("Metrics disabled") 162 | metricsDisabled = true 163 | return 164 | } 165 | 166 | log.Info("Starting up Metrics server ", cfg.Bind) 167 | defineMetrics() 168 | 169 | prometheus.MustRegister(buildInfo) 170 | buildInfo.WithLabelValues(info.Version, info.Revision, info.Branch, runtime.Version()).Set(1) 171 | 172 | prometheus.MustRegister(serverCount) 173 | prometheus.MustRegister(serverActiveConnections) 174 | prometheus.MustRegister(serverRxTotal) 175 | prometheus.MustRegister(serverTxTotal) 176 | prometheus.MustRegister(serverRxSecond) 177 | prometheus.MustRegister(serverTxSecond) 178 | 179 | prometheus.MustRegister(backendActiveConnections) 180 | prometheus.MustRegister(backendRefusedConnections) 181 | prometheus.MustRegister(backendTotalConnections) 182 | prometheus.MustRegister(backendRxBytes) 183 | prometheus.MustRegister(backendTxBytes) 184 | prometheus.MustRegister(backendRxSecond) 185 | prometheus.MustRegister(backendTxSecond) 186 | prometheus.MustRegister(backendLive) 187 | 188 | http.Handle("/metrics", promhttp.Handler()) 189 | go func() { 190 | log.Errorf("Failed to listen and serve prometeus metrics endpoint: %v", http.ListenAndServe(cfg.Bind, nil)) 191 | }() 192 | } 193 | 194 | func RemoveServer(server string, backends map[core.Target]*core.Backend) { 195 | if metricsDisabled { 196 | return 197 | } 198 | 199 | serverCount.DeleteLabelValues(server) 200 | serverActiveConnections.DeleteLabelValues(server) 201 | serverRxTotal.DeleteLabelValues(server) 202 | serverTxTotal.DeleteLabelValues(server) 203 | serverRxSecond.DeleteLabelValues(server) 204 | serverTxSecond.DeleteLabelValues(server) 205 | 206 | for _, backend := range backends { 207 | RemoveBackend(server, backend) 208 | } 209 | } 210 | 211 | func RemoveBackend(server string, backend *core.Backend) { 212 | if metricsDisabled { 213 | return 214 | } 215 | 216 | backendActiveConnections.DeleteLabelValues(server, backend.Host, backend.Port) 217 | backendRefusedConnections.DeleteLabelValues(server, backend.Host, backend.Port) 218 | backendTotalConnections.DeleteLabelValues(server, backend.Host, backend.Port) 219 | backendRxBytes.DeleteLabelValues(server, backend.Host, backend.Port) 220 | backendTxBytes.DeleteLabelValues(server, backend.Host, backend.Port) 221 | backendRxSecond.DeleteLabelValues(server, backend.Host, backend.Port) 222 | backendTxSecond.DeleteLabelValues(server, backend.Host, backend.Port) 223 | backendLive.DeleteLabelValues(server, backend.Host, backend.Port) 224 | } 225 | 226 | func ReportHandleBackendLiveChange(server string, target core.Target, live bool) { 227 | if metricsDisabled { 228 | return 229 | } 230 | 231 | intLive := int(0) 232 | if live { 233 | intLive = 1 234 | } 235 | 236 | backendLive.WithLabelValues(server, target.Host, target.Port).Set(float64(intLive)) 237 | } 238 | 239 | func ReportHandleConnectionsChange(server string, connections uint) { 240 | if metricsDisabled { 241 | return 242 | } 243 | 244 | serverActiveConnections.WithLabelValues(server).Set(float64(connections)) 245 | } 246 | 247 | func ReportHandleStatsChange(server string, bs counters.BandwidthStats) { 248 | if metricsDisabled { 249 | return 250 | } 251 | 252 | serverRxTotal.WithLabelValues(server).Set(float64(bs.RxTotal)) 253 | serverTxTotal.WithLabelValues(server).Set(float64(bs.TxTotal)) 254 | serverRxSecond.WithLabelValues(server).Set(float64(bs.RxSecond)) 255 | serverTxSecond.WithLabelValues(server).Set(float64(bs.TxSecond)) 256 | } 257 | 258 | func ReportHandleBackendStatsChange(server string, target core.Target, backends map[core.Target]*core.Backend) { 259 | if metricsDisabled { 260 | return 261 | } 262 | 263 | backend, _ := backends[target] 264 | 265 | serverCount.WithLabelValues(server).Set(float64(len(backends))) 266 | 267 | backendRxBytes.WithLabelValues(server, target.Host, target.Port).Set(float64(backend.Stats.RxBytes)) 268 | backendTxBytes.WithLabelValues(server, target.Host, target.Port).Set(float64(backend.Stats.TxBytes)) 269 | backendRxSecond.WithLabelValues(server, target.Host, target.Port).Set(float64(backend.Stats.RxSecond)) 270 | backendTxSecond.WithLabelValues(server, target.Host, target.Port).Set(float64(backend.Stats.TxSecond)) 271 | } 272 | 273 | func ReportHandleOp(server string, target core.Target, backends map[core.Target]*core.Backend) { 274 | if metricsDisabled { 275 | return 276 | } 277 | 278 | backend, _ := backends[target] 279 | 280 | backendActiveConnections.WithLabelValues(server, target.Host, target.Port).Set(float64(backend.Stats.ActiveConnections)) 281 | backendRefusedConnections.WithLabelValues(server, target.Host, target.Port).Set(float64(backend.Stats.RefusedConnections)) 282 | backendTotalConnections.WithLabelValues(server, target.Host, target.Port).Set(float64(backend.Stats.TotalConnections)) 283 | } 284 | -------------------------------------------------------------------------------- /src/server/modules/access/access.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | /** 4 | * access.go - access 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "errors" 11 | "net" 12 | 13 | "github.com/yyyar/gobetween/config" 14 | ) 15 | 16 | /** 17 | * Access defines access rules chain 18 | */ 19 | type Access struct { 20 | AllowDefault bool 21 | Rules []AccessRule 22 | } 23 | 24 | /** 25 | * Creates new Access based on config 26 | */ 27 | func NewAccess(cfg *config.AccessConfig) (*Access, error) { 28 | 29 | if cfg == nil { 30 | return nil, errors.New("AccessConfig is nil") 31 | } 32 | 33 | if cfg.Default == "" { 34 | cfg.Default = "allow" 35 | } 36 | 37 | if cfg.Default != "allow" && cfg.Default != "deny" { 38 | return nil, errors.New("AccessConfig Unexpected Default: " + cfg.Default) 39 | } 40 | 41 | access := Access{ 42 | AllowDefault: cfg.Default == "allow", 43 | Rules: []AccessRule{}, 44 | } 45 | 46 | // Parse rules 47 | for _, r := range cfg.Rules { 48 | rule, err := ParseAccessRule(r) 49 | if err != nil { 50 | return nil, err 51 | } 52 | access.Rules = append(access.Rules, *rule) 53 | } 54 | 55 | return &access, nil 56 | } 57 | 58 | /** 59 | * Checks if ip is allowed 60 | */ 61 | func (this *Access) Allows(ip *net.IP) bool { 62 | 63 | for _, r := range this.Rules { 64 | if r.Matches(ip) { 65 | return r.Allows() 66 | } 67 | } 68 | 69 | return this.AllowDefault 70 | } 71 | -------------------------------------------------------------------------------- /src/server/modules/access/rule.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | /** 4 | * rule.go - access rule 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "errors" 11 | "net" 12 | "strings" 13 | ) 14 | 15 | /** 16 | * AccessRule defines order (access, deny) 17 | * and IP or Network 18 | */ 19 | type AccessRule struct { 20 | Allow bool 21 | IsNetwork bool 22 | Ip *net.IP 23 | Network *net.IPNet 24 | } 25 | 26 | /** 27 | * Parses string to AccessRule 28 | */ 29 | func ParseAccessRule(rule string) (*AccessRule, error) { 30 | 31 | parts := strings.Split(rule, " ") 32 | if len(parts) != 2 { 33 | return nil, errors.New("Bad access rule format: " + rule) 34 | } 35 | 36 | r := parts[0] 37 | cidrOrIp := parts[1] 38 | 39 | if r != "allow" && r != "deny" { 40 | return nil, errors.New("Cant parse rule definition " + rule) 41 | } 42 | 43 | // try check if cidrOrIp is ip and handle 44 | 45 | ipShould := net.ParseIP(cidrOrIp) 46 | if ipShould != nil { 47 | return &AccessRule{ 48 | Allow: r == "allow", 49 | Ip: &ipShould, 50 | IsNetwork: false, 51 | Network: nil, 52 | }, nil 53 | } 54 | 55 | _, ipNetShould, _ := net.ParseCIDR(cidrOrIp) 56 | if ipNetShould != nil { 57 | return &AccessRule{ 58 | Allow: r == "allow", 59 | Ip: nil, 60 | IsNetwork: true, 61 | Network: ipNetShould, 62 | }, nil 63 | } 64 | 65 | return nil, errors.New("Cant parse acces rule target, not an ip or cidr: " + cidrOrIp) 66 | 67 | } 68 | 69 | /** 70 | * Checks if ip matches access rule 71 | */ 72 | func (this *AccessRule) Matches(ip *net.IP) bool { 73 | 74 | switch this.IsNetwork { 75 | case true: 76 | return this.Network.Contains(*ip) 77 | case false: 78 | return (*this.Ip).Equal(*ip) 79 | } 80 | 81 | return false 82 | } 83 | 84 | /** 85 | * Checks is it's allow or deny rule 86 | */ 87 | func (this *AccessRule) Allows() bool { 88 | return this.Allow 89 | } 90 | -------------------------------------------------------------------------------- /src/server/scheduler/scheduler.go: -------------------------------------------------------------------------------- 1 | package scheduler 2 | 3 | /** 4 | * scheduler.go - schedule operations on backends and manages them 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "fmt" 11 | "time" 12 | 13 | "github.com/yyyar/gobetween/core" 14 | "github.com/yyyar/gobetween/discovery" 15 | "github.com/yyyar/gobetween/healthcheck" 16 | "github.com/yyyar/gobetween/logging" 17 | "github.com/yyyar/gobetween/metrics" 18 | "github.com/yyyar/gobetween/stats" 19 | "github.com/yyyar/gobetween/stats/counters" 20 | ) 21 | 22 | /** 23 | * Backend Operation action 24 | */ 25 | type OpAction int 26 | 27 | /** 28 | * Constants for backend operation 29 | */ 30 | const ( 31 | IncrementConnection OpAction = iota 32 | DecrementConnection 33 | IncrementRefused 34 | IncrementTx 35 | IncrementRx 36 | ) 37 | 38 | /** 39 | * Operation on backend 40 | */ 41 | type Op struct { 42 | target core.Target 43 | op OpAction 44 | param interface{} 45 | } 46 | 47 | /** 48 | * Request to elect backend 49 | */ 50 | type ElectRequest struct { 51 | Context core.Context 52 | Response chan core.Backend 53 | Err chan error 54 | } 55 | 56 | /** 57 | * Scheduler 58 | */ 59 | type Scheduler struct { 60 | 61 | /* Balancer impl */ 62 | Balancer core.Balancer 63 | 64 | /* Discovery impl */ 65 | Discovery *discovery.Discovery 66 | 67 | /* Healthcheck impl */ 68 | Healthcheck *healthcheck.Healthcheck 69 | 70 | /* ----- backends ------*/ 71 | 72 | /* Current cached backends map */ 73 | backends map[core.Target]*core.Backend 74 | 75 | /* Stats */ 76 | StatsHandler *stats.Handler 77 | 78 | /* ----- channels ----- */ 79 | 80 | /* Backend operation channel */ 81 | ops chan Op 82 | 83 | /* Stop channel */ 84 | stop chan bool 85 | 86 | /* Elect backend channel */ 87 | elect chan ElectRequest 88 | } 89 | 90 | /** 91 | * Start scheduler 92 | */ 93 | func (this *Scheduler) Start() { 94 | 95 | log := logging.For("scheduler") 96 | 97 | log.Info("Starting scheduler ", this.StatsHandler.Name) 98 | 99 | this.ops = make(chan Op) 100 | this.elect = make(chan ElectRequest) 101 | this.stop = make(chan bool) 102 | this.backends = make(map[core.Target]*core.Backend) 103 | 104 | this.Discovery.Start() 105 | this.Healthcheck.Start() 106 | 107 | // backends stats pusher ticker 108 | backendsPushTicker := time.NewTicker(2 * time.Second) 109 | 110 | /** 111 | * Goroutine updates and manages backends 112 | */ 113 | go func() { 114 | for { 115 | select { 116 | 117 | /* ----- discovery ----- */ 118 | 119 | // handle newly discovered backends 120 | case backends := <-this.Discovery.Discover(): 121 | this.HandleBackendsUpdate(backends) 122 | this.Healthcheck.In <- this.Targets() 123 | this.StatsHandler.BackendsCounter.In <- this.Targets() 124 | 125 | /* ------ healthcheck ----- */ 126 | 127 | // handle backend healthcheck result 128 | case checkResult := <-this.Healthcheck.Out: 129 | this.HandleBackendLiveChange(checkResult.Target, checkResult.Status == healthcheck.Healthy) 130 | 131 | /* ----- stats ----- */ 132 | 133 | // push current backends to stats handler 134 | case <-backendsPushTicker.C: 135 | this.StatsHandler.Backends <- this.Backends() 136 | 137 | // handle new bandwidth stats of a backend 138 | case bs := <-this.StatsHandler.BackendsCounter.Out: 139 | this.HandleBackendStatsChange(bs.Target, &bs) 140 | 141 | /* ----- operations ----- */ 142 | 143 | // handle backend operation 144 | case op := <-this.ops: 145 | this.HandleOp(op) 146 | 147 | // elect backend 148 | case electReq := <-this.elect: 149 | this.HandleBackendElect(electReq) 150 | 151 | /* ----- stop ----- */ 152 | 153 | // handle scheduler stop 154 | case <-this.stop: 155 | log.Info("Stopping scheduler ", this.StatsHandler.Name) 156 | backendsPushTicker.Stop() 157 | this.Discovery.Stop() 158 | this.Healthcheck.Stop() 159 | metrics.RemoveServer(fmt.Sprintf("%s", this.StatsHandler.Name), this.backends) 160 | return 161 | } 162 | } 163 | }() 164 | } 165 | 166 | /** 167 | * Returns targets of current backends 168 | */ 169 | func (this *Scheduler) Targets() []core.Target { 170 | 171 | keys := make([]core.Target, 0, len(this.backends)) 172 | for k := range this.backends { 173 | keys = append(keys, k) 174 | } 175 | 176 | return keys 177 | } 178 | 179 | /** 180 | * Return current backends 181 | */ 182 | func (this *Scheduler) Backends() []core.Backend { 183 | 184 | backends := make([]core.Backend, 0, len(this.backends)) 185 | for _, b := range this.backends { 186 | backends = append(backends, *b) 187 | } 188 | 189 | return backends 190 | } 191 | 192 | /** 193 | * Updated backend stats 194 | */ 195 | func (this *Scheduler) HandleBackendStatsChange(target core.Target, bs *counters.BandwidthStats) { 196 | 197 | backend, ok := this.backends[target] 198 | if !ok { 199 | logging.For("scheduler").Warn("No backends for checkResult ", target) 200 | return 201 | } 202 | 203 | backend.Stats.RxBytes = bs.RxTotal 204 | backend.Stats.TxBytes = bs.TxTotal 205 | backend.Stats.RxSecond = bs.RxSecond 206 | backend.Stats.TxSecond = bs.TxSecond 207 | 208 | metrics.ReportHandleBackendStatsChange(fmt.Sprintf("%s", this.StatsHandler.Name), target, this.backends) 209 | } 210 | 211 | /** 212 | * Updated backend live status 213 | */ 214 | func (this *Scheduler) HandleBackendLiveChange(target core.Target, live bool) { 215 | 216 | backend, ok := this.backends[target] 217 | if !ok { 218 | logging.For("scheduler").Warn("No backends for checkResult ", target) 219 | return 220 | } 221 | 222 | backend.Stats.Live = live 223 | 224 | metrics.ReportHandleBackendLiveChange(fmt.Sprintf("%s", this.StatsHandler.Name), target, live) 225 | } 226 | 227 | /** 228 | * Update backends map 229 | */ 230 | func (this *Scheduler) HandleBackendsUpdate(backends []core.Backend) { 231 | 232 | // first mark all existing backends as not discovered 233 | for _, b := range this.backends { 234 | b.Stats.Discovered = false 235 | } 236 | 237 | for _, b := range backends { 238 | oldB, ok := this.backends[b.Target] 239 | 240 | if ok { 241 | // if we have this backend, update it's discovery properties 242 | oldB.MergeFrom(b) 243 | // mark found backend as discovered 244 | oldB.Stats.Discovered = true 245 | continue 246 | } 247 | 248 | b := b // b has to be local variable in order to make unique pointers 249 | b.Stats.Discovered = true 250 | this.backends[b.Target] = &b 251 | 252 | b.Stats.Live = this.Healthcheck.InitialBackendHealthCheckStatus() == healthcheck.Healthy 253 | } 254 | 255 | //remove not discovered backends without active connections 256 | for t, b := range this.backends { 257 | if b.Stats.Discovered || b.Stats.ActiveConnections > 0 { 258 | continue 259 | } 260 | 261 | metrics.RemoveBackend(this.StatsHandler.Name, b) 262 | 263 | delete(this.backends, t) 264 | } 265 | } 266 | 267 | /** 268 | * Perform backend election 269 | */ 270 | func (this *Scheduler) HandleBackendElect(req ElectRequest) { 271 | 272 | // Filter only live and discovered backends 273 | var backends []*core.Backend 274 | for _, b := range this.backends { 275 | 276 | if !b.Stats.Live { 277 | continue 278 | } 279 | 280 | if !b.Stats.Discovered { 281 | continue 282 | } 283 | 284 | backends = append(backends, b) 285 | } 286 | 287 | // Elect backend 288 | backend, err := this.Balancer.Elect(req.Context, backends) 289 | if err != nil { 290 | req.Err <- err 291 | return 292 | } 293 | 294 | req.Response <- *backend 295 | } 296 | 297 | /** 298 | * Handle operation on the backend 299 | */ 300 | func (this *Scheduler) HandleOp(op Op) { 301 | 302 | // Increment global counter, even if 303 | // backend for this count may be out of discovery pool 304 | switch op.op { 305 | case IncrementTx: 306 | this.StatsHandler.Traffic <- core.ReadWriteCount{CountWrite: op.param.(uint), Target: op.target} 307 | return 308 | case IncrementRx: 309 | this.StatsHandler.Traffic <- core.ReadWriteCount{CountRead: op.param.(uint), Target: op.target} 310 | return 311 | } 312 | 313 | log := logging.For("scheduler") 314 | 315 | backend, ok := this.backends[op.target] 316 | if !ok { 317 | log.Warn("Trying op ", op.op, " on not tracked target ", op.target) 318 | return 319 | } 320 | 321 | switch op.op { 322 | case IncrementRefused: 323 | backend.Stats.RefusedConnections++ 324 | case IncrementConnection: 325 | backend.Stats.ActiveConnections++ 326 | backend.Stats.TotalConnections++ 327 | case DecrementConnection: 328 | backend.Stats.ActiveConnections-- 329 | default: 330 | log.Warn("Don't know how to handle op ", op.op) 331 | } 332 | 333 | metrics.ReportHandleOp(fmt.Sprintf("%s", this.StatsHandler.Name), op.target, this.backends) 334 | } 335 | 336 | /** 337 | * Stop scheduler 338 | */ 339 | func (this *Scheduler) Stop() { 340 | this.stop <- true 341 | } 342 | 343 | /** 344 | * Take elect backend for proxying 345 | */ 346 | func (this *Scheduler) TakeBackend(context core.Context) (*core.Backend, error) { 347 | r := ElectRequest{context, make(chan core.Backend), make(chan error)} 348 | this.elect <- r 349 | select { 350 | case err := <-r.Err: 351 | return nil, err 352 | case backend := <-r.Response: 353 | return &backend, nil 354 | } 355 | } 356 | 357 | /** 358 | * Increment connection refused count for backend 359 | */ 360 | func (this *Scheduler) IncrementRefused(backend core.Backend) { 361 | this.ops <- Op{backend.Target, IncrementRefused, nil} 362 | } 363 | 364 | /** 365 | * Increment backend connection counter 366 | */ 367 | func (this *Scheduler) IncrementConnection(backend core.Backend) { 368 | this.ops <- Op{backend.Target, IncrementConnection, nil} 369 | } 370 | 371 | /** 372 | * Decrement backends connection counter 373 | */ 374 | func (this *Scheduler) DecrementConnection(backend core.Backend) { 375 | this.ops <- Op{backend.Target, DecrementConnection, nil} 376 | } 377 | 378 | /** 379 | * Increment Rx stats for backend 380 | */ 381 | func (this *Scheduler) IncrementRx(backend core.Backend, c uint) { 382 | this.ops <- Op{backend.Target, IncrementRx, c} 383 | } 384 | 385 | /** 386 | * Increment Tx stats for backends 387 | */ 388 | func (this *Scheduler) IncrementTx(backend core.Backend, c uint) { 389 | this.ops <- Op{backend.Target, IncrementTx, c} 390 | } 391 | -------------------------------------------------------------------------------- /src/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | /** 4 | * server.go - server creator 5 | * 6 | * @author Illarion Kovalchuk 7 | * @author Yaroslav Pogrebnyak 8 | */ 9 | 10 | import ( 11 | "errors" 12 | 13 | "github.com/yyyar/gobetween/config" 14 | "github.com/yyyar/gobetween/core" 15 | "github.com/yyyar/gobetween/server/tcp" 16 | "github.com/yyyar/gobetween/server/udp" 17 | ) 18 | 19 | /** 20 | * Creates new Server based on cfg.Protocol 21 | */ 22 | func New(name string, cfg config.Server) (core.Server, error) { 23 | switch cfg.Protocol { 24 | case "tls", "tcp": 25 | return tcp.New(name, cfg) 26 | case "udp": 27 | return udp.New(name, cfg) 28 | default: 29 | return nil, errors.New("Can't create server for protocol " + cfg.Protocol) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/server/tcp/proxy.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | /** 4 | * proxy.go - proxy utils 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "io" 11 | "net" 12 | "time" 13 | 14 | "github.com/yyyar/gobetween/core" 15 | "github.com/yyyar/gobetween/logging" 16 | ) 17 | 18 | const ( 19 | 20 | /* Buffer size to handle data from socket */ 21 | BUFFER_SIZE = 16 * 1024 22 | 23 | /* Interval of pushing aggregated read/write stats */ 24 | PROXY_STATS_PUSH_INTERVAL = 1 * time.Second 25 | ) 26 | 27 | /** 28 | * Perform copy/proxy data from 'from' to 'to' socket, counting r/w stats and 29 | * dropping connection if timeout exceeded 30 | */ 31 | func proxy(to net.Conn, from net.Conn, timeout time.Duration) <-chan core.ReadWriteCount { 32 | 33 | log := logging.For("proxy") 34 | 35 | stats := make(chan core.ReadWriteCount) 36 | outStats := make(chan core.ReadWriteCount) 37 | 38 | rwcBuffer := core.ReadWriteCount{} 39 | ticker := time.NewTicker(PROXY_STATS_PUSH_INTERVAL) 40 | flushed := false 41 | 42 | // Stats collecting goroutine 43 | go func() { 44 | 45 | if timeout > 0 { 46 | from.SetReadDeadline(time.Now().Add(timeout)) 47 | } 48 | 49 | for { 50 | select { 51 | case <-ticker.C: 52 | if !rwcBuffer.IsZero() { 53 | outStats <- rwcBuffer 54 | } 55 | flushed = true 56 | case rwc, ok := <-stats: 57 | 58 | if !ok { 59 | ticker.Stop() 60 | if !flushed && !rwcBuffer.IsZero() { 61 | outStats <- rwcBuffer 62 | } 63 | close(outStats) 64 | return 65 | } 66 | 67 | if timeout > 0 && rwc.CountRead > 0 { 68 | from.SetReadDeadline(time.Now().Add(timeout)) 69 | } 70 | 71 | // Remove non blocking 72 | if flushed { 73 | rwcBuffer = rwc 74 | } else { 75 | rwcBuffer.CountWrite += rwc.CountWrite 76 | rwcBuffer.CountRead += rwc.CountRead 77 | } 78 | 79 | flushed = false 80 | } 81 | } 82 | }() 83 | 84 | // Run proxy copier 85 | go func() { 86 | err := Copy(to, from, stats) 87 | // hack to determine normal close. TODO: fix when it will be exposed in golang 88 | e, ok := err.(*net.OpError) 89 | if err != nil && (!ok || e.Err.Error() != "use of closed network connection") { 90 | log.Warn(err) 91 | } 92 | 93 | to.Close() 94 | from.Close() 95 | 96 | // Stop stats collecting goroutine 97 | close(stats) 98 | }() 99 | 100 | return outStats 101 | } 102 | 103 | /** 104 | * It's build by analogy of io.Copy 105 | */ 106 | func Copy(to io.Writer, from io.Reader, ch chan<- core.ReadWriteCount) error { 107 | 108 | buf := make([]byte, BUFFER_SIZE) 109 | var err error = nil 110 | 111 | for { 112 | readN, readErr := from.Read(buf) 113 | 114 | if readN > 0 { 115 | 116 | writeN, writeErr := to.Write(buf[0:readN]) 117 | 118 | if writeN > 0 { 119 | ch <- core.ReadWriteCount{CountRead: uint(readN), CountWrite: uint(writeN)} 120 | } 121 | 122 | if writeErr != nil { 123 | err = writeErr 124 | break 125 | } 126 | 127 | if readN != writeN { 128 | err = io.ErrShortWrite 129 | break 130 | } 131 | } 132 | 133 | if readErr == io.EOF { 134 | break 135 | } 136 | 137 | if readErr != nil { 138 | err = readErr 139 | break 140 | } 141 | } 142 | 143 | return err 144 | } 145 | -------------------------------------------------------------------------------- /src/server/tcp/server.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | /** 4 | * server.go - proxy server implementation 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "crypto/tls" 11 | "net" 12 | "time" 13 | 14 | "github.com/yyyar/gobetween/balance" 15 | "github.com/yyyar/gobetween/config" 16 | "github.com/yyyar/gobetween/core" 17 | "github.com/yyyar/gobetween/discovery" 18 | "github.com/yyyar/gobetween/healthcheck" 19 | "github.com/yyyar/gobetween/logging" 20 | "github.com/yyyar/gobetween/server/modules/access" 21 | "github.com/yyyar/gobetween/server/scheduler" 22 | "github.com/yyyar/gobetween/stats" 23 | "github.com/yyyar/gobetween/utils" 24 | "github.com/yyyar/gobetween/utils/proxyprotocol" 25 | tlsutil "github.com/yyyar/gobetween/utils/tls" 26 | "github.com/yyyar/gobetween/utils/tls/sni" 27 | ) 28 | 29 | /** 30 | * Server listens for client connections and 31 | * proxies it to backends 32 | */ 33 | type Server struct { 34 | 35 | /* Server friendly name */ 36 | name string 37 | 38 | /* Listener */ 39 | listener net.Listener 40 | 41 | /* Configuration */ 42 | cfg config.Server 43 | 44 | /* Scheduler deals with discovery, balancing and healthchecks */ 45 | scheduler scheduler.Scheduler 46 | 47 | /* Current clients connection */ 48 | clients map[string]net.Conn 49 | 50 | /* Stats handler */ 51 | statsHandler *stats.Handler 52 | 53 | /* ----- channels ----- */ 54 | 55 | /* Channel for new connections */ 56 | connect chan (*core.TcpContext) 57 | 58 | /* Channel for dropping connections or connectons to drop */ 59 | disconnect chan (net.Conn) 60 | 61 | /* Stop channel */ 62 | stop chan bool 63 | 64 | /* Tls config used to connect to backends */ 65 | backendsTlsConfg *tls.Config 66 | 67 | /* Tls config used for incoming connections */ 68 | tlsConfig *tls.Config 69 | 70 | /* Get certificate filled by external service */ 71 | GetCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error) 72 | 73 | /* ----- modules ----- */ 74 | 75 | /* Access module checks if client is allowed to connect */ 76 | access *access.Access 77 | } 78 | 79 | /** 80 | * Creates new server instance 81 | */ 82 | func New(name string, cfg config.Server) (*Server, error) { 83 | 84 | log := logging.For("server") 85 | 86 | var err error = nil 87 | statsHandler := stats.NewHandler(name) 88 | 89 | // Create server 90 | server := &Server{ 91 | name: name, 92 | cfg: cfg, 93 | stop: make(chan bool), 94 | disconnect: make(chan net.Conn), 95 | connect: make(chan *core.TcpContext), 96 | clients: make(map[string]net.Conn), 97 | statsHandler: statsHandler, 98 | scheduler: scheduler.Scheduler{ 99 | Balancer: balance.New(cfg.Sni, cfg.Balance), 100 | Discovery: discovery.New(cfg.Discovery.Kind, *cfg.Discovery), 101 | Healthcheck: healthcheck.New(cfg.Healthcheck.Kind, *cfg.Healthcheck), 102 | StatsHandler: statsHandler, 103 | }, 104 | } 105 | 106 | /* Add access if needed */ 107 | if cfg.Access != nil { 108 | server.access, err = access.NewAccess(cfg.Access) 109 | if err != nil { 110 | return nil, err 111 | } 112 | } 113 | 114 | /* Add tls configs if needed */ 115 | 116 | server.backendsTlsConfg, err = tlsutil.MakeBackendTLSConfig(cfg.BackendsTls) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | log.Info("Creating '", name, "': ", cfg.Bind, " ", cfg.Balance, " ", cfg.Discovery.Kind, " ", cfg.Healthcheck.Kind) 122 | 123 | return server, nil 124 | } 125 | 126 | /** 127 | * Returns current server configuration 128 | */ 129 | func (this *Server) Cfg() config.Server { 130 | return this.cfg 131 | } 132 | 133 | /** 134 | * Start server 135 | */ 136 | func (this *Server) Start() error { 137 | 138 | var err error 139 | this.tlsConfig, err = tlsutil.MakeTlsConfig(this.cfg.Tls, this.GetCertificate) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | go func() { 145 | 146 | for { 147 | select { 148 | case client := <-this.disconnect: 149 | this.HandleClientDisconnect(client) 150 | 151 | case ctx := <-this.connect: 152 | this.HandleClientConnect(ctx) 153 | 154 | case <-this.stop: 155 | this.scheduler.Stop() 156 | this.statsHandler.Stop() 157 | if this.listener != nil { 158 | this.listener.Close() 159 | for _, conn := range this.clients { 160 | conn.Close() 161 | } 162 | } 163 | this.clients = make(map[string]net.Conn) 164 | return 165 | } 166 | } 167 | }() 168 | 169 | // Start stats handler 170 | this.statsHandler.Start() 171 | 172 | // Start scheduler 173 | this.scheduler.Start() 174 | 175 | // Start listening 176 | if err := this.Listen(); err != nil { 177 | this.Stop() 178 | return err 179 | } 180 | 181 | return nil 182 | } 183 | 184 | /** 185 | * Handle client disconnection 186 | */ 187 | func (this *Server) HandleClientDisconnect(client net.Conn) { 188 | client.Close() 189 | delete(this.clients, client.RemoteAddr().String()) 190 | this.statsHandler.Connections <- uint(len(this.clients)) 191 | } 192 | 193 | /** 194 | * Handle new client connection 195 | */ 196 | func (this *Server) HandleClientConnect(ctx *core.TcpContext) { 197 | client := ctx.Conn 198 | log := logging.For("server") 199 | 200 | if *this.cfg.MaxConnections != 0 && len(this.clients) >= *this.cfg.MaxConnections { 201 | log.Warn("Too many connections to ", this.cfg.Bind) 202 | client.Close() 203 | return 204 | } 205 | 206 | this.clients[client.RemoteAddr().String()] = client 207 | this.statsHandler.Connections <- uint(len(this.clients)) 208 | go func() { 209 | this.handle(ctx) 210 | this.disconnect <- client 211 | }() 212 | } 213 | 214 | /** 215 | * Stop, dropping all connections 216 | */ 217 | func (this *Server) Stop() { 218 | 219 | log := logging.For("server.Listen") 220 | log.Info("Stopping ", this.name) 221 | 222 | this.stop <- true 223 | } 224 | 225 | func (this *Server) wrap(conn net.Conn, sniEnabled bool) { 226 | log := logging.For("server.Listen.wrap") 227 | 228 | var hostname string 229 | var err error 230 | 231 | if sniEnabled { 232 | var sniConn net.Conn 233 | sniConn, hostname, err = sni.Sniff(conn, utils.ParseDurationOrDefault(this.cfg.Sni.ReadTimeout, time.Second*2)) 234 | 235 | if err != nil { 236 | log.Error("Failed to get / parse ClientHello for sni: ", err) 237 | conn.Close() 238 | return 239 | } 240 | 241 | conn = sniConn 242 | } 243 | 244 | if this.tlsConfig != nil { 245 | conn = tls.Server(conn, this.tlsConfig) 246 | } 247 | 248 | this.connect <- &core.TcpContext{ 249 | hostname, 250 | conn, 251 | } 252 | 253 | } 254 | 255 | /** 256 | * Listen on specified port for a connections 257 | */ 258 | func (this *Server) Listen() (err error) { 259 | 260 | log := logging.For("server.Listen") 261 | 262 | // create tcp listener 263 | this.listener, err = net.Listen("tcp", this.cfg.Bind) 264 | 265 | if err != nil { 266 | log.Error("Error starting ", this.cfg.Protocol+" server: ", err) 267 | return err 268 | } 269 | 270 | sniEnabled := this.cfg.Sni != nil 271 | 272 | go func() { 273 | for { 274 | conn, err := this.listener.Accept() 275 | 276 | if err != nil { 277 | log.Error(err) 278 | return 279 | } 280 | 281 | go this.wrap(conn, sniEnabled) 282 | } 283 | }() 284 | 285 | return nil 286 | } 287 | 288 | /** 289 | * Handle incoming connection and prox it to backend 290 | */ 291 | func (this *Server) handle(ctx *core.TcpContext) { 292 | clientConn := ctx.Conn 293 | log := logging.For("server.handle [" + this.cfg.Bind + "]") 294 | 295 | /* Check access if needed */ 296 | if this.access != nil { 297 | if !this.access.Allows(&clientConn.RemoteAddr().(*net.TCPAddr).IP) { 298 | log.Debug("Client disallowed to connect ", clientConn.RemoteAddr()) 299 | clientConn.Close() 300 | return 301 | } 302 | } 303 | 304 | log.Debug("Accepted ", clientConn.RemoteAddr(), " -> ", this.listener.Addr()) 305 | 306 | /* Find out backend for proxying */ 307 | var err error 308 | backend, err := this.scheduler.TakeBackend(ctx) 309 | if err != nil { 310 | log.Error(err, "; Closing connection: ", clientConn.RemoteAddr()) 311 | return 312 | } 313 | 314 | /* Connect to backend */ 315 | var backendConn net.Conn 316 | 317 | if this.cfg.BackendsTls != nil { 318 | backendConn, err = tls.DialWithDialer(&net.Dialer{ 319 | Timeout: utils.ParseDurationOrDefault(*this.cfg.BackendConnectionTimeout, 0), 320 | }, "tcp", backend.Address(), this.backendsTlsConfg) 321 | 322 | } else { 323 | backendConn, err = net.DialTimeout("tcp", backend.Address(), utils.ParseDurationOrDefault(*this.cfg.BackendConnectionTimeout, 0)) 324 | } 325 | 326 | if err != nil { 327 | this.scheduler.IncrementRefused(*backend) 328 | log.Error(err) 329 | return 330 | } 331 | this.scheduler.IncrementConnection(*backend) 332 | defer this.scheduler.DecrementConnection(*backend) 333 | 334 | /* Send proxy protocol header if configured */ 335 | if this.cfg.ProxyProtocol != nil { 336 | switch this.cfg.ProxyProtocol.Version { 337 | case "1": 338 | log.Debug("Sending proxy_protocol v1 header ", clientConn.RemoteAddr(), " -> ", this.listener.Addr(), " -> ", backendConn.RemoteAddr()) 339 | err := proxyprotocol.SendProxyProtocolV1(clientConn, backendConn) 340 | if err != nil { 341 | log.Error(err) 342 | return 343 | } 344 | default: 345 | log.Error("Unsupported proxy_protocol version " + this.cfg.ProxyProtocol.Version + ", aborting connection") 346 | return 347 | } 348 | } 349 | 350 | /* ----- Stat proxying ----- */ 351 | 352 | log.Debug("Begin ", clientConn.RemoteAddr(), " -> ", this.listener.Addr(), " -> ", backendConn.RemoteAddr()) 353 | cs := proxy(clientConn, backendConn, utils.ParseDurationOrDefault(*this.cfg.BackendIdleTimeout, 0)) 354 | bs := proxy(backendConn, clientConn, utils.ParseDurationOrDefault(*this.cfg.ClientIdleTimeout, 0)) 355 | 356 | isTx, isRx := true, true 357 | for isTx || isRx { 358 | select { 359 | case s, ok := <-cs: 360 | isRx = ok 361 | if !ok { 362 | cs = nil 363 | continue 364 | } 365 | this.scheduler.IncrementRx(*backend, s.CountWrite) 366 | case s, ok := <-bs: 367 | isTx = ok 368 | if !ok { 369 | bs = nil 370 | continue 371 | } 372 | this.scheduler.IncrementTx(*backend, s.CountWrite) 373 | } 374 | } 375 | 376 | log.Debug("End ", clientConn.RemoteAddr(), " -> ", this.listener.Addr(), " -> ", backendConn.RemoteAddr()) 377 | } 378 | -------------------------------------------------------------------------------- /src/server/udp/session/config.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "time" 4 | 5 | type Config struct { 6 | MaxRequests uint64 7 | MaxResponses uint64 8 | ClientIdleTimeout time.Duration 9 | BackendIdleTimeout time.Duration 10 | Transparent bool 11 | } 12 | -------------------------------------------------------------------------------- /src/server/udp/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/yyyar/gobetween/core" 11 | "github.com/yyyar/gobetween/logging" 12 | "github.com/yyyar/gobetween/server/scheduler" 13 | ) 14 | 15 | const ( 16 | UDP_PACKET_SIZE = 65507 17 | MAX_PACKETS_QUEUE = 10000 18 | ) 19 | 20 | var log = logging.For("udp/server/session") 21 | var bufPool = sync.Pool{ 22 | New: func() interface{} { 23 | return make([]byte, UDP_PACKET_SIZE) 24 | }, 25 | } 26 | 27 | type packet struct { 28 | // pointer to object that has to be returned to buf pool 29 | payload []byte 30 | // length of the usable part of buffer 31 | len int 32 | } 33 | 34 | func (p packet) buf() []byte { 35 | if p.payload == nil { 36 | return nil 37 | } 38 | 39 | return p.payload[0:p.len] 40 | } 41 | 42 | func (p packet) release() { 43 | if p.payload == nil { 44 | return 45 | } 46 | bufPool.Put(p.payload) 47 | } 48 | 49 | type Session struct { 50 | //counters 51 | sent uint64 52 | recv uint64 53 | 54 | //session config 55 | cfg Config 56 | 57 | clientAddr *net.UDPAddr 58 | 59 | //connection to backend 60 | conn net.Conn 61 | backend core.Backend 62 | 63 | //communication 64 | out chan packet 65 | stopC chan struct{} 66 | stopped uint32 67 | 68 | //scheduler 69 | scheduler *scheduler.Scheduler 70 | } 71 | 72 | func NewSession(clientAddr *net.UDPAddr, conn net.Conn, backend core.Backend, scheduler *scheduler.Scheduler, cfg Config) *Session { 73 | 74 | scheduler.IncrementConnection(backend) 75 | s := &Session{ 76 | cfg: cfg, 77 | clientAddr: clientAddr, 78 | conn: conn, 79 | backend: backend, 80 | scheduler: scheduler, 81 | out: make(chan packet, MAX_PACKETS_QUEUE), 82 | stopC: make(chan struct{}, 1), 83 | } 84 | 85 | go func() { 86 | 87 | var t *time.Timer 88 | var tC <-chan time.Time 89 | 90 | if cfg.ClientIdleTimeout > 0 { 91 | t = time.NewTimer(cfg.ClientIdleTimeout) 92 | tC = t.C 93 | } 94 | 95 | for { 96 | select { 97 | 98 | case <-tC: 99 | s.Close() 100 | case pkt := <-s.out: 101 | if t != nil { 102 | if !t.Stop() { 103 | <-t.C 104 | } 105 | t.Reset(cfg.ClientIdleTimeout) 106 | } 107 | 108 | if pkt.payload == nil { 109 | panic("Program error, output channel should not be closed here") 110 | } 111 | 112 | n, err := s.conn.Write(pkt.buf()) 113 | pkt.release() 114 | 115 | if err != nil { 116 | log.Errorf("Could not write data to udp connection: %v", err) 117 | break 118 | } 119 | 120 | if n != pkt.len { 121 | log.Errorf("Short write error: should write %d bytes, but %d written", pkt.len, n) 122 | break 123 | } 124 | 125 | s.scheduler.IncrementTx(s.backend, uint(n)) 126 | 127 | if s.cfg.MaxRequests > 0 && atomic.AddUint64(&s.sent, 1) > s.cfg.MaxRequests { 128 | log.Errorf("Restricted to send more UDP packets") 129 | break 130 | } 131 | case <-s.stopC: 132 | atomic.StoreUint32(&s.stopped, 1) 133 | if t != nil { 134 | t.Stop() 135 | } 136 | s.conn.Close() 137 | s.scheduler.DecrementConnection(s.backend) 138 | // drain output packets channel and free buffers 139 | for { 140 | select { 141 | case pkt := <-s.out: 142 | pkt.release() 143 | default: 144 | return 145 | } 146 | } 147 | 148 | } 149 | } 150 | 151 | }() 152 | 153 | return s 154 | } 155 | 156 | func (s *Session) Write(buf []byte) error { 157 | if atomic.LoadUint32(&s.stopped) == 1 { 158 | return fmt.Errorf("Closed session") 159 | } 160 | 161 | dup := bufPool.Get().([]byte) 162 | n := copy(dup, buf) 163 | 164 | select { 165 | case s.out <- packet{dup, n}: 166 | default: 167 | bufPool.Put(dup) 168 | } 169 | 170 | return nil 171 | } 172 | 173 | /** 174 | * ListenResponses waits for responses from backend, and sends them back to client address via 175 | * server connection, so that client is not confused with source host:port of the 176 | * packet it receives 177 | */ 178 | func (s *Session) ListenResponses(sendTo *net.UDPConn) { 179 | 180 | go func() { 181 | b := make([]byte, UDP_PACKET_SIZE) 182 | 183 | defer s.Close() 184 | 185 | for { 186 | 187 | if s.cfg.BackendIdleTimeout > 0 { 188 | s.conn.SetReadDeadline(time.Now().Add(s.cfg.BackendIdleTimeout)) 189 | } 190 | 191 | n, err := s.conn.Read(b) 192 | 193 | if err != nil { 194 | if err, ok := err.(net.Error); ok && err.Timeout() { 195 | return 196 | } 197 | 198 | if atomic.LoadUint32(&s.stopped) == 0 { 199 | log.Errorf("Failed to read from backend: %v", err) 200 | } 201 | return 202 | } 203 | 204 | s.scheduler.IncrementRx(s.backend, uint(n)) 205 | 206 | m, err := sendTo.WriteToUDP(b[0:n], s.clientAddr) 207 | 208 | if err != nil { 209 | log.Errorf("Could not send backend response to client: %v", err) 210 | return 211 | } 212 | 213 | if m != n { 214 | return 215 | } 216 | 217 | if s.cfg.MaxResponses > 0 && atomic.AddUint64(&s.recv, 1) >= s.cfg.MaxResponses { 218 | return 219 | } 220 | } 221 | }() 222 | } 223 | 224 | func (s *Session) IsDone() bool { 225 | return atomic.LoadUint32(&s.stopped) == 1 226 | } 227 | 228 | func (s *Session) Close() { 229 | select { 230 | case s.stopC <- struct{}{}: 231 | default: 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/service/acme.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/yyyar/gobetween/config" 10 | "github.com/yyyar/gobetween/core" 11 | "github.com/yyyar/gobetween/server/tcp" 12 | "golang.org/x/crypto/acme/autocert" 13 | ) 14 | 15 | /** 16 | * AcmeService listens on http port (default 80) for incoming acme challenges from letsencrypt.org 17 | * and updates it's certificate manager's hotpolicy depending on acme hosts configured for 18 | * each core.Server instance with [acme] section in config 19 | */ 20 | type AcmeService struct { 21 | certMan *autocert.Manager 22 | hosts map[string]bool 23 | sync.RWMutex 24 | } 25 | 26 | func init() { 27 | registry["acme"] = NewAcmeService 28 | } 29 | 30 | func NewAcmeService(cfg config.Config) core.Service { 31 | 32 | if cfg.Acme == nil { 33 | return nil 34 | } 35 | 36 | a := &AcmeService{ 37 | certMan: &autocert.Manager{ 38 | Cache: autocert.DirCache(cfg.Acme.CacheDir), 39 | Prompt: autocert.AcceptTOS, 40 | }, 41 | hosts: make(map[string]bool), 42 | } 43 | 44 | a.certMan.HostPolicy = func(_ context.Context, host string) error { 45 | a.RLock() 46 | defer a.RUnlock() 47 | 48 | if a.hosts[host] { 49 | return nil 50 | } 51 | 52 | return fmt.Errorf("Acme: host %s is not configured", host) 53 | } 54 | 55 | //accept http challenge 56 | if cfg.Acme.Challenge == "http" { 57 | go http.ListenAndServe(cfg.Acme.HttpBind, a.certMan.HTTPHandler(nil)) 58 | } 59 | 60 | return a 61 | 62 | } 63 | 64 | func (a *AcmeService) Enable(server core.Server) error { 65 | 66 | if a == nil { 67 | return nil 68 | } 69 | 70 | serverCfg := server.Cfg() 71 | 72 | if serverCfg.Tls == nil { 73 | return nil 74 | } 75 | 76 | tcpServer, ok := server.(*tcp.Server) 77 | 78 | if !ok { 79 | return nil 80 | } 81 | 82 | tcpServer.GetCertificate = a.certMan.GetCertificate 83 | 84 | a.Lock() 85 | defer a.Unlock() 86 | 87 | for _, host := range serverCfg.Tls.AcmeHosts { 88 | 89 | if a.hosts[host] { 90 | return fmt.Errorf("Acme host %s is already configured", host) 91 | } 92 | 93 | a.hosts[host] = true 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (a *AcmeService) Disable(server core.Server) error { 100 | 101 | serverCfg := server.Cfg() 102 | 103 | if serverCfg.Tls == nil { 104 | return nil 105 | } 106 | 107 | a.Lock() 108 | defer a.Unlock() 109 | 110 | for _, host := range serverCfg.Tls.AcmeHosts { 111 | delete(a.hosts, host) 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /src/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/yyyar/gobetween/config" 5 | "github.com/yyyar/gobetween/core" 6 | "github.com/yyyar/gobetween/logging" 7 | ) 8 | 9 | /** 10 | * Registry of factory methods for Services 11 | */ 12 | var registry = make(map[string]func(config.Config) core.Service) 13 | 14 | func All(cfg config.Config) []core.Service { 15 | log := logging.For("services") 16 | 17 | result := make([]core.Service, 0) 18 | 19 | for name, constructor := range registry { 20 | service := constructor(cfg) 21 | if service == nil { 22 | continue 23 | } 24 | log.Info("Creating ", name) 25 | result = append(result, service) 26 | } 27 | 28 | return result 29 | } 30 | -------------------------------------------------------------------------------- /src/stats/counters/backendscounter.go: -------------------------------------------------------------------------------- 1 | package counters 2 | 3 | /** 4 | * backendscounter.go - bandwidth counter for backends pool 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/yyyar/gobetween/core" 13 | ) 14 | 15 | const ( 16 | /* Stats update interval */ 17 | INTERVAL = 2 * time.Second 18 | ) 19 | 20 | /** 21 | * Bandwidth counter for backends pool 22 | */ 23 | type BackendsBandwidthCounter struct { 24 | 25 | /* Map of counters of specific targets */ 26 | counters map[core.Target]*BandwidthCounter 27 | 28 | /* ----- channels ------ */ 29 | 30 | /* Input channel of updated targets */ 31 | In chan []core.Target 32 | 33 | /* Input channel of traffic deltas */ 34 | Traffic chan core.ReadWriteCount 35 | 36 | /* Output channel for counted stats */ 37 | Out chan BandwidthStats 38 | 39 | /* Stop channel */ 40 | stop chan bool 41 | } 42 | 43 | /** 44 | * Creates new backends bandwidth counter 45 | */ 46 | func NewBackendsBandwidthCounter() *BackendsBandwidthCounter { 47 | return &BackendsBandwidthCounter{ 48 | counters: make(map[core.Target]*BandwidthCounter), 49 | In: make(chan []core.Target), 50 | Traffic: make(chan core.ReadWriteCount), 51 | Out: make(chan BandwidthStats), 52 | stop: make(chan bool), 53 | } 54 | } 55 | 56 | /** 57 | * Start backends counter 58 | */ 59 | func (this *BackendsBandwidthCounter) Start() { 60 | 61 | go func() { 62 | for { 63 | select { 64 | 65 | // stop 66 | case <-this.stop: 67 | 68 | // Stop all counters 69 | for i := range this.counters { 70 | this.counters[i].Stop() 71 | } 72 | this.counters = nil 73 | 74 | // close channels 75 | close(this.In) 76 | close(this.Traffic) 77 | close(this.Out) 78 | return 79 | 80 | // new backends available 81 | case targets := <-this.In: 82 | this.UpdateCounters(targets) 83 | 84 | // new traffic available 85 | // route to appropriated counter 86 | case rwc := <-this.Traffic: 87 | counter, ok := this.counters[rwc.Target] 88 | // ignore stats for backend that is not is list 89 | if ok { 90 | counter.Traffic <- rwc 91 | } 92 | } 93 | 94 | } 95 | }() 96 | } 97 | 98 | /** 99 | * Update counters to match targets, optionally creating new 100 | * and deleting old counters 101 | */ 102 | func (this *BackendsBandwidthCounter) UpdateCounters(targets []core.Target) { 103 | 104 | result := map[core.Target]*BandwidthCounter{} 105 | 106 | // Keep or add needed workers 107 | for _, t := range targets { 108 | c, ok := this.counters[t] 109 | if !ok { 110 | c = NewBandwidthCounter(INTERVAL, this.Out) 111 | c.Target = t 112 | c.Start() 113 | } 114 | result[t] = c 115 | } 116 | 117 | // Stop needed counters 118 | for currentT, c := range this.counters { 119 | remove := true 120 | for _, t := range targets { 121 | if currentT.EqualTo(t) { 122 | remove = false 123 | break 124 | } 125 | } 126 | 127 | if remove { 128 | c.Stop() 129 | } 130 | } 131 | 132 | this.counters = result 133 | } 134 | 135 | /** 136 | * Stop backends counter 137 | */ 138 | func (this *BackendsBandwidthCounter) Stop() { 139 | this.stop <- true 140 | } 141 | -------------------------------------------------------------------------------- /src/stats/counters/bandwidth.go: -------------------------------------------------------------------------------- 1 | package counters 2 | 3 | /** 4 | * stats.go - bandwidth stats 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "github.com/yyyar/gobetween/core" 11 | ) 12 | 13 | /** 14 | * Bandwidth stats object 15 | */ 16 | type BandwidthStats struct { 17 | 18 | // Total received bytes 19 | RxTotal uint64 20 | 21 | // Total transmitted bytes 22 | TxTotal uint64 23 | 24 | // Received bytes per second 25 | RxSecond uint 26 | 27 | // Transmitted bytes per second 28 | TxSecond uint 29 | 30 | // Optional target of stats 31 | Target core.Target 32 | } 33 | -------------------------------------------------------------------------------- /src/stats/counters/counter.go: -------------------------------------------------------------------------------- 1 | package counters 2 | 3 | /** 4 | * counter.go - bandwidth counter 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/yyyar/gobetween/core" 13 | ) 14 | 15 | /** 16 | * Count total bandwidth and bandwidth per second 17 | */ 18 | type BandwidthCounter struct { 19 | 20 | /* Bandwidth Stats */ 21 | BandwidthStats 22 | 23 | /* Last received total bytes */ 24 | RxTotalLast uint64 25 | /* Last transmitted total bytes */ 26 | TxTotalLast uint64 27 | 28 | /* Timeframe to calculate per-second bandwidth */ 29 | interval time.Duration 30 | /* Ticker for per-second bandwidth calculation and pushing stats */ 31 | ticker *time.Ticker 32 | 33 | /* Indicates that new bandwidth delta was received */ 34 | newTxRx bool 35 | 36 | /* ----- channels ----- */ 37 | 38 | /* Input channel for bandwidth deltas */ 39 | Traffic chan core.ReadWriteCount 40 | 41 | /* Stop channel */ 42 | stop chan bool 43 | 44 | /* Output channel for bandwidth stats */ 45 | Out chan BandwidthStats 46 | } 47 | 48 | /** 49 | * Create new BandwidthCounter 50 | */ 51 | func NewBandwidthCounter(interval time.Duration, out chan BandwidthStats) *BandwidthCounter { 52 | 53 | return &BandwidthCounter{ 54 | interval: interval, 55 | ticker: time.NewTicker(interval), 56 | BandwidthStats: BandwidthStats{ 57 | RxTotal: 0, 58 | TxTotal: 0, 59 | }, 60 | TxTotalLast: 0, 61 | RxTotalLast: 0, 62 | Out: out, 63 | Traffic: make(chan core.ReadWriteCount), 64 | stop: make(chan bool), 65 | } 66 | } 67 | 68 | /** 69 | * Starts bandwidth counter 70 | */ 71 | func (this *BandwidthCounter) Start() { 72 | 73 | go func() { 74 | 75 | for { 76 | select { 77 | 78 | // Stop requested 79 | case <-this.stop: 80 | this.ticker.Stop() 81 | close(this.Traffic) 82 | return 83 | 84 | // New counting cycle 85 | case <-this.ticker.C: 86 | 87 | if !this.newTxRx { 88 | this.RxSecond = 0 89 | this.TxSecond = 0 90 | } else { 91 | 92 | dRx := this.RxTotal - this.RxTotalLast 93 | dTx := this.TxTotal - this.TxTotalLast 94 | 95 | this.RxSecond = uint(dRx / uint64(this.interval.Seconds())) 96 | this.TxSecond = uint(dTx / uint64(this.interval.Seconds())) 97 | 98 | this.RxTotalLast = this.RxTotal 99 | this.TxTotalLast = this.TxTotal 100 | 101 | this.newTxRx = false 102 | } 103 | 104 | // Send results to out 105 | this.Out <- this.BandwidthStats 106 | 107 | // New traffic deltas available 108 | case rwc := <-this.Traffic: 109 | this.newTxRx = true 110 | this.RxTotal += uint64(rwc.CountRead) 111 | this.TxTotal += uint64(rwc.CountWrite) 112 | } 113 | } 114 | }() 115 | } 116 | 117 | /** 118 | * Stops bandwidth counter 119 | */ 120 | func (this *BandwidthCounter) Stop() { 121 | this.stop <- true 122 | } 123 | -------------------------------------------------------------------------------- /src/stats/handler.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | /** 4 | * handler.go - server stats handler 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "fmt" 11 | "time" 12 | 13 | "github.com/yyyar/gobetween/core" 14 | "github.com/yyyar/gobetween/metrics" 15 | "github.com/yyyar/gobetween/stats/counters" 16 | ) 17 | 18 | const ( 19 | /* Stats update interval */ 20 | INTERVAL = 2 * time.Second 21 | ) 22 | 23 | /** 24 | * Handler processess data from server 25 | */ 26 | type Handler struct { 27 | 28 | /* Server's name */ 29 | Name string 30 | 31 | /* Server counter */ 32 | serverCounter *counters.BandwidthCounter 33 | /* Backends counters */ 34 | BackendsCounter *counters.BackendsBandwidthCounter 35 | 36 | /* Current stats */ 37 | latestStats Stats 38 | 39 | /* ----- channels ----- */ 40 | 41 | /* Server traffic data */ 42 | Traffic chan core.ReadWriteCount 43 | 44 | /* Server current connections count */ 45 | Connections chan uint 46 | 47 | /* Current backends pool */ 48 | Backends chan []core.Backend 49 | 50 | /* Channel for indicating stop request */ 51 | stopChan chan bool 52 | 53 | /* Input channel for latest stats */ 54 | ServerStats chan counters.BandwidthStats 55 | } 56 | 57 | /** 58 | * Creates new stats handler for the server 59 | * with name 'name' 60 | */ 61 | func NewHandler(name string) *Handler { 62 | 63 | handler := &Handler{ 64 | Name: name, 65 | ServerStats: make(chan counters.BandwidthStats, 1), 66 | Traffic: make(chan core.ReadWriteCount), 67 | Connections: make(chan uint), 68 | Backends: make(chan []core.Backend), 69 | stopChan: make(chan bool), 70 | latestStats: Stats{ 71 | RxTotal: 0, 72 | TxTotal: 0, 73 | RxSecond: 0, 74 | TxSecond: 0, 75 | Backends: []core.Backend{}, 76 | }, 77 | } 78 | 79 | handler.serverCounter = counters.NewBandwidthCounter(INTERVAL, handler.ServerStats) 80 | handler.BackendsCounter = counters.NewBackendsBandwidthCounter() 81 | 82 | Store.Lock() 83 | Store.handlers[name] = handler 84 | Store.Unlock() 85 | 86 | return handler 87 | } 88 | 89 | /** 90 | * Start handler work asynchroniously 91 | */ 92 | func (this *Handler) Start() { 93 | 94 | this.serverCounter.Start() 95 | this.BackendsCounter.Start() 96 | 97 | go func() { 98 | 99 | for { 100 | select { 101 | 102 | /* stop stats processor requested */ 103 | case <-this.stopChan: 104 | 105 | this.serverCounter.Stop() 106 | this.BackendsCounter.Stop() 107 | 108 | Store.Lock() 109 | delete(Store.handlers, this.Name) 110 | Store.Unlock() 111 | 112 | // close channels 113 | close(this.ServerStats) 114 | close(this.Traffic) 115 | close(this.Connections) 116 | return 117 | 118 | /* New server stats available */ 119 | case b := <-this.ServerStats: 120 | this.latestStats.RxTotal = b.RxTotal 121 | this.latestStats.TxTotal = b.TxTotal 122 | this.latestStats.RxSecond = b.RxSecond 123 | this.latestStats.TxSecond = b.TxSecond 124 | 125 | metrics.ReportHandleStatsChange(fmt.Sprintf("%s", this.Name), b) 126 | 127 | /* New server backends with stats available */ 128 | case backends := <-this.Backends: 129 | this.latestStats.Backends = backends 130 | 131 | /* New sever connections count available */ 132 | case connections := <-this.Connections: 133 | this.latestStats.ActiveConnections = connections 134 | 135 | metrics.ReportHandleConnectionsChange(fmt.Sprintf("%s", this.Name), connections) 136 | 137 | /* New traffic stats available */ 138 | case rwc := <-this.Traffic: 139 | // forward to counters 140 | go func() { 141 | this.serverCounter.Traffic <- rwc 142 | this.BackendsCounter.Traffic <- rwc 143 | }() 144 | } 145 | } 146 | }() 147 | 148 | } 149 | 150 | /** 151 | * Request handler stop and clear resources 152 | */ 153 | func (this *Handler) Stop() { 154 | this.stopChan <- true 155 | } 156 | -------------------------------------------------------------------------------- /src/stats/stats.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | /** 4 | * stats.go - server stats object 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "github.com/yyyar/gobetween/core" 11 | ) 12 | 13 | /** 14 | * Stats of the Server 15 | */ 16 | type Stats struct { 17 | 18 | /* Current active client connections */ 19 | ActiveConnections uint `json:"active_connections"` 20 | 21 | /* Total received bytes from backend */ 22 | RxTotal uint64 `json:"rx_total"` 23 | 24 | /* Total transmitter bytes to backend */ 25 | TxTotal uint64 `json:"tx_total"` 26 | 27 | /* Received bytes to backend / second */ 28 | RxSecond uint `json:"rx_second"` 29 | 30 | /* Transmitted bytes to backend / second */ 31 | TxSecond uint `json:"tx_second"` 32 | 33 | /* Current backends pool */ 34 | Backends []core.Backend `json:"backends"` 35 | } 36 | -------------------------------------------------------------------------------- /src/stats/store.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | /** 4 | * store.go - stats storage and getter 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "sync" 11 | ) 12 | 13 | /** 14 | * Handlers Store 15 | */ 16 | var Store = struct { 17 | sync.RWMutex 18 | handlers map[string]*Handler 19 | }{handlers: make(map[string]*Handler)} 20 | 21 | /** 22 | * Get stats for the server 23 | */ 24 | func GetStats(name string) interface{} { 25 | 26 | Store.RLock() 27 | defer Store.RUnlock() 28 | 29 | handler, ok := Store.handlers[name] 30 | if !ok { 31 | return nil 32 | } 33 | return handler.latestStats // TODO: syncronize? 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/codec/codec.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | /** 4 | * codec.go - decoding utils 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "bytes" 11 | "encoding/json" 12 | "errors" 13 | 14 | "github.com/burntsushi/toml" 15 | ) 16 | 17 | /** 18 | * Encode data based on format 19 | * Currently supported: toml and json 20 | */ 21 | func Encode(in interface{}, out *string, format string) error { 22 | 23 | switch format { 24 | case "toml": 25 | buf := new(bytes.Buffer) 26 | if err := toml.NewEncoder(buf).Encode(in); err != nil { 27 | return err 28 | } 29 | *out = buf.String() 30 | return nil 31 | case "json": 32 | buf, err := json.MarshalIndent(in, "", " ") 33 | if err != nil { 34 | return err 35 | } 36 | *out = string(buf) 37 | return nil 38 | default: 39 | return errors.New("Unknown format " + format) 40 | } 41 | } 42 | 43 | /** 44 | * Decode data based on format 45 | * Currently supported: toml and json 46 | */ 47 | func Decode(data string, out interface{}, format string) error { 48 | 49 | switch format { 50 | case "toml": 51 | _, err := toml.Decode(data, out) 52 | return err 53 | case "json": 54 | return json.Unmarshal([]byte(data), out) 55 | default: 56 | return errors.New("Unknown format " + format) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/env.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | /** 4 | * env.go - env vars helpers 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "os" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | // 16 | // SubstituteEnvVars replaces placeholders ${...} with env var value 17 | // 18 | func SubstituteEnvVars(data string) string { 19 | 20 | var re = regexp.MustCompile(`\${.*?}`) 21 | 22 | vars := re.FindAllString(data, -1) 23 | for _, v := range vars { 24 | data = strings.ReplaceAll(data, v, os.Getenv(v[2:len(v)-1])) 25 | } 26 | return data 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/exec.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | /** 4 | * exec.go - Exec external process with timeout 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | * @author Ievgen Ponomarenko 8 | */ 9 | 10 | import ( 11 | "os/exec" 12 | "time" 13 | 14 | "github.com/yyyar/gobetween/logging" 15 | ) 16 | 17 | /** 18 | * Exec with timeout 19 | */ 20 | func ExecTimeout(timeout time.Duration, params ...string) (string, error) { 21 | 22 | log := logging.For("execTimeout") 23 | 24 | cmd := exec.Command(params[0], params[1:]...) 25 | 26 | timer := time.AfterFunc(timeout, func() { 27 | if cmd.Process != nil { 28 | log.Info("Response from exec ", params, " is timed out. Killing process...") 29 | cmd.Process.Kill() 30 | } 31 | }) 32 | 33 | out, err := cmd.Output() 34 | timer.Stop() 35 | 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | return string(out), nil 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/parsers/backend.go: -------------------------------------------------------------------------------- 1 | package parsers 2 | 3 | /** 4 | * backend.go - backend parser utils 5 | * 6 | * @author Ievgen Ponomarenko 7 | * @author Yaroslav Pogrebnyak 8 | */ 9 | 10 | import ( 11 | "errors" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/yyyar/gobetween/core" 17 | ) 18 | 19 | const ( 20 | DEFAULT_BACKEND_PATTERN = `^(?P\S+):(?P\d+)(\sweight=(?P\d+))?(\spriority=(?P\d+))?(\smax_connections=(?P\d+))?(\ssni=(?P[^\s]+))?$` 21 | ) 22 | 23 | /** 24 | * Do parding of backend line with default pattern 25 | */ 26 | func ParseBackendDefault(line string) (*core.Backend, error) { 27 | return ParseBackend(line, DEFAULT_BACKEND_PATTERN) 28 | } 29 | 30 | /** 31 | * Do parsing of backend line 32 | */ 33 | func ParseBackend(line string, pattern string) (*core.Backend, error) { 34 | 35 | //trim string 36 | line = strings.TrimSpace(line) 37 | 38 | // parse string by regexp 39 | var reg = regexp.MustCompile(pattern) 40 | match := reg.FindStringSubmatch(line) 41 | 42 | if len(match) == 0 { 43 | return nil, errors.New("Cant parse " + line) 44 | } 45 | 46 | result := make(map[string]string) 47 | 48 | // get named capturing groups 49 | for i, name := range reg.SubexpNames() { 50 | if name != "" { 51 | result[name] = match[i] 52 | } 53 | } 54 | 55 | weight, err := strconv.Atoi(result["weight"]) 56 | if err != nil { 57 | weight = 1 58 | } 59 | 60 | priority, err := strconv.Atoi(result["priority"]) 61 | if err != nil { 62 | priority = 1 63 | } 64 | 65 | maxConnections, err := strconv.Atoi(result["max_connections"]) 66 | if err != nil { 67 | maxConnections = 0 // 0 means no limit 68 | } 69 | 70 | backend := core.Backend{ 71 | Target: core.Target{ 72 | Host: result["host"], 73 | Port: result["port"], 74 | }, 75 | Weight: weight, 76 | MaxConnections: maxConnections, 77 | Sni: result["sni"], 78 | Priority: priority, 79 | Stats: core.BackendStats{ 80 | Live: true, 81 | }, 82 | } 83 | 84 | return &backend, nil 85 | } -------------------------------------------------------------------------------- /src/utils/pidfile/pidfile.go: -------------------------------------------------------------------------------- 1 | package pidfile 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strconv" 8 | "syscall" 9 | ) 10 | 11 | func WritePidFile(path string) error { 12 | _, err := os.Stat(path) 13 | 14 | if err == nil { // file already exists 15 | data, err := ioutil.ReadFile(path) 16 | if err != nil { 17 | return fmt.Errorf("Could not read %s: %v", path, err) 18 | } 19 | 20 | pid, err := strconv.Atoi(string(data)) 21 | if err != nil { 22 | return fmt.Errorf("Could not parse pid file %s contents '%s': %v", path, string(data), err) 23 | } 24 | 25 | if process, err := os.FindProcess(pid); err == nil { 26 | if err := process.Signal(syscall.Signal(0)); err == nil { 27 | return fmt.Errorf("process with pid %d is still running", pid) 28 | } 29 | } 30 | } 31 | 32 | return ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", os.Getpid())), 0664) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/profiler/profiler.go: -------------------------------------------------------------------------------- 1 | package profiler 2 | 3 | import ( 4 | "net/http" 5 | _ "net/http/pprof" 6 | 7 | "github.com/yyyar/gobetween/logging" 8 | ) 9 | 10 | func Start(bind string) { 11 | log := logging.For("profiler") 12 | 13 | log.Infof("Starting profiler: %v", bind) 14 | 15 | go func() { 16 | log.Error(http.ListenAndServe(bind, nil)) 17 | }() 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/proxyprotocol/proxyprotocol.go: -------------------------------------------------------------------------------- 1 | package proxyprotocol 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | 8 | proxyproto "github.com/pires/go-proxyproto" 9 | ) 10 | 11 | func addrToIPAndPort(addr net.Addr) (ip net.IP, port uint16, err error) { 12 | ipString, portString, err := net.SplitHostPort(addr.String()) 13 | if err != nil { 14 | return 15 | } 16 | 17 | ip = net.ParseIP(ipString) 18 | if ip == nil { 19 | err = fmt.Errorf("Could not parse IP") 20 | return 21 | } 22 | 23 | p, err := strconv.ParseInt(portString, 10, 64) 24 | if err != nil { 25 | return 26 | } 27 | port = uint16(p) 28 | return 29 | } 30 | 31 | // / SendProxyProtocolV1 sends a proxy protocol v1 header to initialize the connection 32 | // / https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt 33 | func SendProxyProtocolV1(client net.Conn, backend net.Conn) error { 34 | sourceIP, _, err := addrToIPAndPort(client.RemoteAddr()) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | _, _, err = addrToIPAndPort(client.LocalAddr()) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | h := proxyproto.Header{ 45 | Version: 1, 46 | SourceAddr: client.RemoteAddr(), 47 | DestinationAddr: client.LocalAddr(), 48 | } 49 | if sourceIP.To4() != nil { 50 | h.TransportProtocol = proxyproto.TCPv4 51 | } else { 52 | h.TransportProtocol = proxyproto.TCPv6 53 | } 54 | 55 | _, err = h.WriteTo(backend) 56 | if err != nil { 57 | return nil 58 | } 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/time.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | /** 4 | * time.go - Time utils 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "time" 11 | ) 12 | 13 | /** 14 | * Parse duration or return default 15 | */ 16 | func ParseDurationOrDefault(s string, defaultDuration time.Duration) time.Duration { 17 | 18 | var d time.Duration 19 | var err error 20 | 21 | if s == "" { 22 | return defaultDuration 23 | } 24 | 25 | d, err = time.ParseDuration(s) 26 | if err != nil { 27 | return defaultDuration 28 | } 29 | 30 | return d 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/tls/sni/extract.go: -------------------------------------------------------------------------------- 1 | package sni 2 | 3 | /** 4 | * extract.go - extractor of hostname from ClientHello 5 | * 6 | * @author Illarion Kovalchuk 7 | */ 8 | 9 | import ( 10 | "bytes" 11 | "crypto/tls" 12 | "io" 13 | "net" 14 | "time" 15 | ) 16 | 17 | type bufferConn struct { 18 | io.Reader 19 | } 20 | 21 | type localAddr struct{} 22 | 23 | func (l localAddr) String() string { 24 | return "127.0.0.1" 25 | } 26 | 27 | func (l localAddr) Network() string { 28 | return "tcp" 29 | } 30 | 31 | func newBufferConn(b []byte) *bufferConn { 32 | return &bufferConn{bytes.NewReader(b)} 33 | } 34 | 35 | func (c bufferConn) Write(b []byte) (n int, err error) { 36 | return 0, nil 37 | } 38 | 39 | func (c bufferConn) Close() error { 40 | return nil 41 | } 42 | 43 | func (c bufferConn) LocalAddr() net.Addr { 44 | return localAddr{} 45 | } 46 | 47 | func (c bufferConn) RemoteAddr() net.Addr { 48 | return localAddr{} 49 | } 50 | 51 | func (c bufferConn) SetDeadline(t time.Time) error { 52 | return nil 53 | } 54 | 55 | func (c bufferConn) SetReadDeadline(t time.Time) error { 56 | return nil 57 | } 58 | 59 | func (c bufferConn) SetWriteDeadline(t time.Time) error { 60 | return nil 61 | } 62 | 63 | func extractHostname(buf []byte) string { 64 | conn := tls.Server(newBufferConn(buf), &tls.Config{}) 65 | defer conn.Close() 66 | conn.Handshake() 67 | return conn.ConnectionState().ServerName 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/tls/sni/sni.go: -------------------------------------------------------------------------------- 1 | package sni 2 | 3 | /** 4 | * sni.go - sni sniffer implementation 5 | * @author Illarion Kovalchuk 6 | * 7 | * Package sni provides transparent access to hostname provided by ClientHello 8 | * message during TLS handshake. 9 | */ 10 | 11 | import ( 12 | "bytes" 13 | "io" 14 | "net" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | const MAX_HEADER_SIZE = 16385 20 | 21 | var pool = sync.Pool{ 22 | New: func() interface{} { 23 | return make([]byte, MAX_HEADER_SIZE) 24 | }, 25 | } 26 | 27 | // Conn delegates all calls to net.Conn, but Read to reader 28 | type Conn struct { 29 | reader io.Reader 30 | net.Conn //delegate 31 | } 32 | 33 | func (c Conn) Read(b []byte) (n int, err error) { 34 | return c.reader.Read(b) 35 | } 36 | 37 | // Sniff sniffs hostname from ClientHello message (if any), 38 | // returns sni.Conn, filling it's Hostname field 39 | func Sniff(conn net.Conn, readTimeout time.Duration) (net.Conn, string, error) { 40 | buf := pool.Get().([]byte) 41 | defer pool.Put(buf) 42 | 43 | err := conn.SetReadDeadline(time.Now().Add(readTimeout)) 44 | if err != nil { 45 | return nil, "", err 46 | } 47 | 48 | i, err := conn.Read(buf) 49 | 50 | if err != nil { 51 | return nil, "", err 52 | } 53 | 54 | err = conn.SetReadDeadline(time.Time{}) // Reset read deadline 55 | if err != nil { 56 | return nil, "", err 57 | } 58 | 59 | hostname := extractHostname(buf[0:i]) 60 | 61 | data := make([]byte, i) 62 | copy(data, buf) // Since we reuse buf between invocations, we have to make copy of data 63 | mreader := io.MultiReader(bytes.NewBuffer(data), conn) 64 | 65 | // Wrap connection so that it will Read from buffer first and remaining data 66 | // from initial conn 67 | return Conn{mreader, conn}, hostname, nil 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/tls/tls.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | /** 4 | * tls.go - Tls mapping utils 5 | * 6 | * @author Yaroslav Pogrebnyak 7 | */ 8 | 9 | import ( 10 | "crypto/tls" 11 | "crypto/x509" 12 | "os" 13 | 14 | "github.com/yyyar/gobetween/config" 15 | ) 16 | 17 | /** 18 | * TLS Ciphers mapping 19 | */ 20 | var suites = map[string]uint16{ 21 | // TLS 1.0 - 1.2 cipher suites 22 | "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, 23 | "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, 24 | "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, 25 | "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, 26 | "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, 27 | "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, 28 | "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, 29 | "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, 30 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 31 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 32 | "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, 33 | "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 34 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 35 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 36 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, 37 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, 38 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 39 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 40 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 41 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 42 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, 43 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, 44 | 45 | // TLS 1.3 cipher suites 46 | "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256, 47 | "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384, 48 | "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, 49 | } 50 | 51 | /** 52 | * TLS Versions mappings 53 | */ 54 | var versions map[string]uint16 = map[string]uint16{ 55 | "tls1": tls.VersionTLS10, 56 | "tls1.1": tls.VersionTLS11, 57 | "tls1.2": tls.VersionTLS12, 58 | "tls1.3": tls.VersionTLS13, 59 | } 60 | 61 | /** 62 | * Maps tls version from string to golang constant 63 | */ 64 | func MapVersion(version string) uint16 { 65 | return versions[version] 66 | } 67 | 68 | /** 69 | * Maps tls ciphers from array of strings to array of golang constants 70 | */ 71 | func MapCiphers(ciphers []string) []uint16 { 72 | 73 | if ciphers == nil || len(ciphers) == 0 { 74 | return nil 75 | } 76 | 77 | result := []uint16{} 78 | 79 | for _, s := range ciphers { 80 | c := suites[s] 81 | if c == 0 { 82 | continue 83 | } 84 | result = append(result, c) 85 | } 86 | 87 | return result 88 | } 89 | 90 | func MakeTlsConfig(tlsC *config.Tls, getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error)) (*tls.Config, error) { 91 | 92 | if tlsC == nil { 93 | return nil, nil 94 | } 95 | 96 | tlsConfig := &tls.Config{} 97 | 98 | tlsConfig.CipherSuites = MapCiphers(tlsC.Ciphers) 99 | tlsConfig.MinVersion = MapVersion(tlsC.MinVersion) 100 | tlsConfig.MaxVersion = MapVersion(tlsC.MaxVersion) 101 | tlsConfig.SessionTicketsDisabled = !tlsC.SessionTickets 102 | 103 | if getCertificate != nil { 104 | tlsConfig.GetCertificate = getCertificate 105 | return tlsConfig, nil 106 | } 107 | 108 | var crt tls.Certificate 109 | var err error 110 | if crt, err = tls.LoadX509KeyPair(tlsC.CertPath, tlsC.KeyPath); err != nil { 111 | return nil, err 112 | } 113 | 114 | tlsConfig.Certificates = []tls.Certificate{crt} 115 | 116 | return tlsConfig, nil 117 | } 118 | 119 | /** 120 | * MakeBackendTLSConfig makes a tls.Config for connecting to backends 121 | */ 122 | func MakeBackendTLSConfig(backendsTls *config.BackendsTls) (*tls.Config, error) { 123 | 124 | if backendsTls == nil { 125 | return nil, nil 126 | } 127 | 128 | var err error 129 | 130 | result := &tls.Config{ 131 | InsecureSkipVerify: backendsTls.IgnoreVerify, 132 | CipherSuites: MapCiphers(backendsTls.Ciphers), 133 | MinVersion: MapVersion(backendsTls.MinVersion), 134 | MaxVersion: MapVersion(backendsTls.MaxVersion), 135 | SessionTicketsDisabled: !backendsTls.SessionTickets, 136 | } 137 | 138 | if backendsTls.CertPath != nil && backendsTls.KeyPath != nil { 139 | 140 | var crt tls.Certificate 141 | 142 | if crt, err = tls.LoadX509KeyPair(*backendsTls.CertPath, *backendsTls.KeyPath); err != nil { 143 | return nil, err 144 | } 145 | 146 | result.Certificates = []tls.Certificate{crt} 147 | } 148 | 149 | if backendsTls.RootCaCertPath != nil { 150 | 151 | var caCertPem []byte 152 | 153 | if caCertPem, err = os.ReadFile(*backendsTls.RootCaCertPath); err != nil { 154 | return nil, err 155 | } 156 | 157 | caCertPool := x509.NewCertPool() 158 | if ok := caCertPool.AppendCertsFromPEM(caCertPem); !ok { 159 | return nil, err 160 | } 161 | 162 | result.RootCAs = caCertPool 163 | 164 | } 165 | 166 | return result, nil 167 | 168 | } 169 | -------------------------------------------------------------------------------- /test/dummy_context.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type DummyContext struct { 9 | ip net.IP 10 | port int 11 | } 12 | 13 | func (d DummyContext) String() string { 14 | return fmt.Sprintf("%v:%v", d.Ip(), d.Port()) 15 | } 16 | 17 | func (d DummyContext) Ip() net.IP { 18 | if d.ip == nil { 19 | d.ip = make(net.IP, 1) 20 | } 21 | return d.ip 22 | } 23 | 24 | func (d DummyContext) Port() int { 25 | return d.port 26 | } 27 | 28 | func (d DummyContext) Sni() string { 29 | return "" 30 | } 31 | -------------------------------------------------------------------------------- /test/iphash_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | 9 | "github.com/yyyar/gobetween/balance" 10 | "github.com/yyyar/gobetween/core" 11 | ) 12 | 13 | func makeDistribution(balancer core.Balancer, backends []*core.Backend, clients []DummyContext) (map[string]*core.Backend, error) { 14 | 15 | result := make(map[string]*core.Backend) 16 | 17 | for _, client := range clients { 18 | electedBackend, err := balancer.Elect(client, backends) 19 | 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | if electedBackend == nil { 25 | return nil, errors.New("Elected nil backend!") 26 | } 27 | 28 | result[client.ip.String()] = electedBackend 29 | } 30 | 31 | return result, nil 32 | 33 | } 34 | 35 | // Prepare list of backends, for testing purposes they end with .1, .2, .3 etc 36 | // It will be easier to print them if needed 37 | func prepareBackends(base string, n int) []*core.Backend { 38 | backends := make([]*core.Backend, n) 39 | 40 | for i := 0; i < n; i++ { 41 | backends[i] = &core.Backend{ 42 | Target: core.Target{ 43 | Host: fmt.Sprintf("%s.%d", base, i+1), 44 | Port: fmt.Sprintf("%d", 1000+i), 45 | }, 46 | } 47 | } 48 | 49 | return backends 50 | } 51 | 52 | // Prepare random list of clients 53 | func prepareClients(n int) []DummyContext { 54 | 55 | clients := make([]DummyContext, n) 56 | 57 | for i := 0; i < n; i++ { 58 | 59 | ip := make([]byte, 4) 60 | rand.Read(ip) 61 | 62 | clients[i] = DummyContext{ 63 | ip: ip, 64 | } 65 | } 66 | 67 | return clients 68 | 69 | } 70 | 71 | //TODO enable test when real consisten hashing will be implemented 72 | /* 73 | func TestIPHash2AddingBackendsRedistribution(t *testing.T) { 74 | rand.Seed(time.Now().Unix()) 75 | balancer := &balance.Iphash2Balancer{} 76 | 77 | N := 50 // initial number of backends 78 | M := 1 // added number of backends 79 | C := 1000 // number of clients 80 | 81 | backends := prepareBackends("127.0.0", N) 82 | clients := prepareClients(C) 83 | 84 | // Perform balancing for on a given balancer, for clients versus backends 85 | d1, err := makeDistribution(balancer, backends, clients) 86 | if err != nil { 87 | t.Error(err) 88 | t.Fail() 89 | } 90 | 91 | extendedBackends := append(backends, prepareBackends("192.168.1", M)...) 92 | 93 | // Perform balancing for on a given balancer, for clients versus extended list of backends 94 | d2, err := makeDistribution(balancer, extendedBackends, clients) 95 | if err != nil { 96 | t.Error(err) 97 | t.Fail() 98 | } 99 | 100 | Q := 0 // number of rehashed clients 101 | 102 | // Q should not be bigger than C/ M+N 103 | 104 | // values should differ 105 | for k, v1 := range d1 { 106 | v2 := d2[k] 107 | if v1 != v2 { 108 | Q++ 109 | } 110 | 111 | } 112 | 113 | if Q > C/(M+N) { 114 | t.Fail() 115 | } 116 | 117 | } 118 | */ 119 | 120 | func TestIPHash1RemovingBackendsStability(t *testing.T) { 121 | 122 | balancer := &balance.Iphash1Balancer{} 123 | 124 | backends := prepareBackends("127.0.0", 4) 125 | clients := prepareClients(100) 126 | 127 | // Perform balancing for on a given balancer, for clients versus backends 128 | d1, err := makeDistribution(balancer, backends, clients) 129 | if err != nil { 130 | t.Error(err) 131 | t.Fail() 132 | } 133 | 134 | // Remove a backend from a list(second one) 135 | removedBackend := backends[1] 136 | backends = append(backends[:1], backends[2:]...) 137 | 138 | // Perform balancing on the same balancer, same clients, but backends missing one. 139 | d2, err := makeDistribution(balancer, backends, clients) 140 | if err != nil { 141 | t.Error(err) 142 | t.Fail() 143 | } 144 | 145 | // check the results 146 | for k, v1 := range d1 { 147 | 148 | // in the second try (d2) removed backend will be obviously changed to something else, 149 | // skipping it 150 | if v1 == removedBackend { 151 | continue 152 | } 153 | 154 | v2 := d2[k] 155 | 156 | // the second try (d2) should not have other changes, so that if some backend (not removed) was 157 | // elected previously, it should be elected now 158 | if v1 != v2 { 159 | t.Fail() 160 | break 161 | } 162 | 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /test/maxconn_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyyar/gobetween/balance" 7 | "github.com/yyyar/gobetween/balance/middleware" 8 | "github.com/yyyar/gobetween/core" 9 | ) 10 | 11 | func TestMaxConnectionsMiddleware(t *testing.T) { 12 | // Create a simple round-robin balancer 13 | balancer := &balance.RoundrobinBalancer{} 14 | 15 | // Wrap it with our MaxConnectionsBalancer middleware 16 | maxConnBalancer := &middleware.MaxConnectionsMiddleware{ 17 | Delegate: balancer, 18 | } 19 | 20 | // Create a test context 21 | context := DummyContext{} 22 | 23 | // Create test backends with different max_connections settings 24 | backends := []*core.Backend{ 25 | { 26 | Target: core.Target{ 27 | Host: "1", 28 | Port: "1", 29 | }, 30 | MaxConnections: 10, 31 | Stats: core.BackendStats{ 32 | ActiveConnections: 5, // Under limit 33 | }, 34 | }, 35 | { 36 | Target: core.Target{ 37 | Host: "2", 38 | Port: "2", 39 | }, 40 | MaxConnections: 10, 41 | Stats: core.BackendStats{ 42 | ActiveConnections: 10, // At limit, should be excluded 43 | }, 44 | }, 45 | { 46 | Target: core.Target{ 47 | Host: "3", 48 | Port: "3", 49 | }, 50 | MaxConnections: 10, 51 | Stats: core.BackendStats{ 52 | ActiveConnections: 15, // Over limit, should be excluded 53 | }, 54 | }, 55 | { 56 | Target: core.Target{ 57 | Host: "4", 58 | Port: "4", 59 | }, 60 | MaxConnections: 0, // No limit set 61 | Stats: core.BackendStats{ 62 | ActiveConnections: 100, // High number of connections, but no limit 63 | }, 64 | }, 65 | } 66 | 67 | // Test 1: Only backends under their limit or with no limit should be elected 68 | selected := make(map[string]bool) 69 | 70 | // Run multiple elections to make sure our middleware is filtering correctly 71 | for i := 0; i < 100; i++ { 72 | backend, err := maxConnBalancer.Elect(context, backends) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | // Add to our list of selected backends 78 | selected[backend.Target.Host] = true 79 | 80 | // Verify the backend is eligible (either under limit or no limit) 81 | if backend.MaxConnections > 0 && backend.Stats.ActiveConnections >= uint(backend.MaxConnections) { 82 | t.Errorf("Backend %s elected despite exceeding max_connections (%d >= %d)", 83 | backend.Target.Host, backend.Stats.ActiveConnections, backend.MaxConnections) 84 | } 85 | } 86 | 87 | // Verify that only backends 1 and 4 were selected 88 | if !selected["1"] { 89 | t.Error("Backend 1 should have been selected (under limit)") 90 | } 91 | if selected["2"] { 92 | t.Error("Backend 2 should NOT have been selected (at limit)") 93 | } 94 | if selected["3"] { 95 | t.Error("Backend 3 should NOT have been selected (over limit)") 96 | } 97 | if !selected["4"] { 98 | t.Error("Backend 4 should have been selected (no limit)") 99 | } 100 | 101 | // Test 2: When all backends exceed their limits, an error should be returned 102 | allLimitedBackends := []*core.Backend{ 103 | { 104 | Target: core.Target{ 105 | Host: "1", 106 | Port: "1", 107 | }, 108 | MaxConnections: 10, 109 | Stats: core.BackendStats{ 110 | ActiveConnections: 10, // At limit 111 | }, 112 | }, 113 | { 114 | Target: core.Target{ 115 | Host: "2", 116 | Port: "2", 117 | }, 118 | MaxConnections: 5, 119 | Stats: core.BackendStats{ 120 | ActiveConnections: 10, // Over limit 121 | }, 122 | }, 123 | } 124 | 125 | _, err := maxConnBalancer.Elect(context, allLimitedBackends) 126 | if err == nil { 127 | t.Error("Expected error when all backends exceed max_connections, but got none") 128 | } 129 | } 130 | 131 | -------------------------------------------------------------------------------- /test/weight_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "testing" 7 | "time" 8 | 9 | "github.com/yyyar/gobetween/balance" 10 | "github.com/yyyar/gobetween/core" 11 | ) 12 | 13 | func TestOnlyBestPriorityBackendsElected(t *testing.T) { 14 | rand.Seed(time.Now().Unix()) 15 | balancer := &balance.WeightBalancer{} 16 | var context core.Context 17 | 18 | context = DummyContext{} 19 | 20 | backends := []*core.Backend{ 21 | { 22 | Priority: 0, 23 | Weight: 0, 24 | }, 25 | { 26 | Priority: 0, 27 | Weight: 1, 28 | }, 29 | { 30 | Priority: 0, 31 | Weight: 2, 32 | }, 33 | { 34 | Priority: 1, 35 | Weight: 0, 36 | }, 37 | { 38 | Priority: 1, 39 | Weight: 1, 40 | }, 41 | { 42 | Priority: 1, 43 | Weight: 2, 44 | }, 45 | { 46 | Priority: 2, 47 | Weight: 0, 48 | }, 49 | { 50 | Priority: 2, 51 | Weight: 1, 52 | }, 53 | { 54 | Priority: 2, 55 | Weight: 2, 56 | }, 57 | } 58 | 59 | hits := make(map[int]bool) 60 | 61 | for try := 0; try < 100; try++ { 62 | backend, err := balancer.Elect(context, backends) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | hits[backend.Priority] = true 68 | if len(hits) > 1 { 69 | t.Error("Backends with different priority elected") 70 | } 71 | 72 | if backend.Priority != 0 { 73 | t.Error("Backends with not optimal priority elected") 74 | } 75 | 76 | } 77 | 78 | } 79 | 80 | func TestAllWeightsEqualTo0Distribution(t *testing.T) { 81 | rand.Seed(time.Now().Unix()) 82 | balancer := &balance.WeightBalancer{} 83 | var context core.Context 84 | 85 | context = DummyContext{} 86 | 87 | backends := []*core.Backend{ 88 | { 89 | Target: core.Target{ 90 | Host: "1", 91 | Port: "1", 92 | }, 93 | Weight: 0, 94 | }, 95 | { 96 | Target: core.Target{ 97 | Host: "2", 98 | Port: "2", 99 | }, 100 | Weight: 0, 101 | }, 102 | { 103 | Target: core.Target{ 104 | Host: "3", 105 | Port: "3", 106 | }, 107 | Weight: 0, 108 | }, 109 | } 110 | 111 | hits := make(map[string]bool) 112 | 113 | for try := 0; try < 100; try++ { 114 | backend, err := balancer.Elect(context, backends) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | 119 | hits[backend.Target.Host] = true 120 | if len(hits) == 3 { 121 | return 122 | } 123 | 124 | } 125 | 126 | if len(hits) != 3 { 127 | t.Error("Group of backends with weight = 0 has some backneds that are never elected") 128 | } 129 | } 130 | 131 | func TestWeightDistribution(t *testing.T) { 132 | rand.Seed(time.Now().Unix()) 133 | balancer := &balance.WeightBalancer{} 134 | var context core.Context 135 | 136 | context = DummyContext{} 137 | 138 | backends := []*core.Backend{ 139 | { 140 | Priority: 1, 141 | Weight: 20, 142 | }, 143 | { 144 | Priority: 1, 145 | Weight: 15, 146 | }, 147 | { 148 | Priority: 1, 149 | Weight: 25, 150 | }, 151 | { 152 | Priority: 1, 153 | Weight: 40, 154 | }, 155 | { 156 | // this backend is ignored 157 | Priority: 2, 158 | Weight: 244, 159 | }, 160 | } 161 | 162 | //shuffle 163 | for s := 0; s < 100; s++ { 164 | i := rand.Intn(len(backends)) 165 | j := rand.Intn(len(backends)) 166 | if i == j { 167 | continue 168 | } 169 | backends[i], backends[j] = backends[j], backends[i] 170 | } 171 | 172 | quantity := make(map[int]int) 173 | 174 | for _, backend := range backends { 175 | if backend.Priority > 1 { 176 | continue 177 | } 178 | quantity[backend.Weight] = 0 179 | } 180 | 181 | n := 10000 182 | for try := 0; try < 100*n; try++ { 183 | backend, err := balancer.Elect(context, backends) 184 | if err != nil { 185 | t.Fatal(err) 186 | } 187 | 188 | quantity[backend.Weight] += 1 189 | } 190 | 191 | for k, v := range quantity { 192 | if math.Abs(float64(v)/float64(n)-float64(k)) > 0.5 { 193 | t.Error(k, ":", float64(v)/float64(n)) 194 | } 195 | 196 | } 197 | } 198 | --------------------------------------------------------------------------------