Initial commit

This commit is contained in:
Geoff Bourne 2018-05-07 22:16:01 -05:00
commit 17a4bd6515
10 changed files with 658 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/vendor/
/.idea/
/*.iml

63
Gopkg.lock generated Normal file
View File

@ -0,0 +1,63 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/alecthomas/kingpin"
packages = ["."]
revision = "947dcec5ba9c011838740e680966fd7087a71d0d"
version = "v2.2.6"
[[projects]]
branch = "master"
name = "github.com/alecthomas/template"
packages = [
".",
"parse"
]
revision = "a0175ee3bccc567396460bf5acd36800cb10c49c"
[[projects]]
branch = "master"
name = "github.com/alecthomas/units"
packages = ["."]
revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
[[projects]]
name = "github.com/gorilla/context"
packages = ["."]
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
version = "v1.1"
[[projects]]
name = "github.com/gorilla/mux"
packages = ["."]
revision = "53c1911da2b537f792e7cafcb446b05ffe33b996"
version = "v1.6.1"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
revision = "4ec37c66abab2c7e02ae775328b2ff001c3f025a"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "6f686a352de66814cdd080d970febae7767857a3"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "8d17b95931fea7bbf18743367ccc152c392add293fe7945ca9ce941ca1482b4f"
solver-name = "gps-cdcl"
solver-version = 1

38
Gopkg.toml Normal file
View File

@ -0,0 +1,38 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "github.com/alecthomas/kingpin"
version = "2.2.6"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "1.0.5"

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Geoff Bourne
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

30
README.md Normal file
View File

@ -0,0 +1,30 @@
Routes Minecraft client connections to backend servers based upon the requested server address.
## Usage
```text
Flags:
--help Show context-sensitive help (also try --help-long
and --help-man).
--port=25565 The port bound to listen for Minecraft client
connections
--api-binding=API-BINDING The host:port bound for servicing API requests
--mapping=MAPPING ... Mapping of external hostname to internal server
host:port
```
## REST API
* `GET /routes`
Retrieves the currently configured routes
* `POST /routes`
Registers a route given a JSON body structured like:
```json
{
"serverAddress": "CLIENT REQUESTED SERVER ADDRESS",
"backend": "HOST:PORT"
}
```
* `DELETE /routes/{serverAddress}`
Deletes an existing route for the given `serverAddress`

42
cmd/mc-router/main.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"net"
"github.com/itzg/mc-router/server"
"github.com/alecthomas/kingpin"
"strconv"
"github.com/sirupsen/logrus"
"context"
"os"
"os/signal"
)
var (
port = kingpin.Flag("port", "The port bound to listen for Minecraft client connections").
Default("25565").Int()
apiBinding = kingpin.Flag("api-binding", "The host:port bound for servicing API requests").
String()
mappings = kingpin.Flag("mapping", "Mapping of external hostname to internal server host:port").
StringMap()
)
func main() {
kingpin.Parse()
ctx, cancel := context.WithCancel(context.Background())
c := make(chan os.Signal, 1)
signal.Notify(c)
server.Routes.RegisterAll(*mappings)
server.Connector.StartAcceptingConnections(ctx, net.JoinHostPort("", strconv.Itoa(*port)))
if *apiBinding != "" {
server.StartApiServer(*apiBinding)
}
<-c
logrus.Info("Stopping")
cancel()
}

168
mcproto/types.go Normal file
View File

@ -0,0 +1,168 @@
package mcproto
import (
"errors"
"bytes"
"strings"
"io"
)
type Frame struct {
Length int
Payload []byte
}
type Packet struct {
Length int
PacketID int
Data []byte
}
const PacketIdHandshake = 0x00
type Handshake struct {
ProtocolVersion int
ServerAddress string
ServerPort uint16
NextState int
}
type ByteReader interface {
ReadByte() (byte, error)
}
func ReadVarInt(reader io.Reader) (int, error) {
b := make([]byte, 1)
var numRead uint = 0
result := 0
for numRead <= 5 {
n, err := reader.Read(b)
if err != nil {
return 0, err
}
if n == 0 {
continue
}
value := b[0] & 0x7F
result |= int(value) << (7*numRead)
numRead++
if b[0] & 0x80 == 0 {
return result, nil
}
}
return 0, errors.New("VarInt is too big")
}
func ReadString(reader io.Reader) (string, error) {
length, err := ReadVarInt(reader)
if err != nil {
return "", err
}
b := make([]byte, 1)
var strBuilder strings.Builder
for i := 0; i < length; i++ {
n, err := reader.Read(b)
if err != nil {
return "", err
}
if n == 0 {
continue
}
strBuilder.WriteByte(b[0])
}
return strBuilder.String(), nil
}
func ReadUnsignedShort(reader io.Reader) (uint16, error) {
upper := make([]byte, 1)
_, err := reader.Read(upper)
if err != nil {
return 0, err
}
lower := make([]byte, 1)
_, err = reader.Read(lower)
if err != nil {
return 0, err
}
return (uint16(upper[0])<<8) | uint16(lower[0]), nil
}
func ReadFrame(reader io.Reader) (*Frame, error) {
var err error
frame := &Frame{}
frame.Length, err = ReadVarInt(reader)
if err != nil {
return nil, err
}
frame.Payload = make([]byte, frame.Length)
total := 0
for total < frame.Length {
readIntoThis := frame.Payload[total:]
n, err := reader.Read(readIntoThis)
if err != nil {
if err != io.EOF {
return nil, err
}
}
total += n
}
return frame, nil
}
func ReadPacket(reader io.Reader) (*Packet, error) {
frame, err := ReadFrame(reader)
if err != nil {
return nil, err
}
packet := &Packet{Length:frame.Length}
remainder := bytes.NewBuffer(frame.Payload)
packet.PacketID, err = ReadVarInt(remainder)
if err != nil {
return nil, err
}
packet.Data = remainder.Bytes()
return packet, nil
}
func ReadHandshake(data []byte) (*Handshake, error) {
handshake := &Handshake{}
buffer := bytes.NewBuffer(data)
var err error
handshake.ProtocolVersion, err = ReadVarInt(buffer)
if err != nil {
return nil, err
}
handshake.ServerAddress, err = ReadString(buffer)
if err != nil {
return nil, err
}
handshake.ServerPort, err = ReadUnsignedShort(buffer)
if err != nil {
return nil, err
}
nextState, err := ReadVarInt(buffer)
if err != nil {
return nil, err
}
handshake.NextState = nextState
return handshake, nil
}

17
server/api_server.go Normal file
View File

@ -0,0 +1,17 @@
package server
import (
"net/http"
"github.com/sirupsen/logrus"
"github.com/gorilla/mux"
)
var apiRoutes = mux.NewRouter()
func StartApiServer(apiBinding string) {
logrus.WithField("binding", apiBinding).Info("Serving API requests")
go func() {
logrus.WithError(
http.ListenAndServe(apiBinding, apiRoutes)).Error("API server failed")
}()
}

144
server/connector.go Normal file
View File

@ -0,0 +1,144 @@
package server
import (
"net"
"github.com/sirupsen/logrus"
"github.com/itzg/mc-router/mcproto"
"context"
"io"
"bytes"
)
type IConnector interface {
StartAcceptingConnections(ctx context.Context, listenAddress string) error
}
var Connector IConnector = &connectorImpl{}
type connectorImpl struct {
}
func (c *connectorImpl) StartAcceptingConnections(ctx context.Context, listenAddress string) error {
ln, err := net.Listen("tcp", listenAddress)
if err != nil {
logrus.WithError(err).Fatal("Unable to start listening")
return err
}
logrus.WithField("listenAddress", listenAddress).Info("Listening for Minecraft client connections")
go c.acceptConnections(ctx, ln)
return nil
}
func (c *connectorImpl) acceptConnections(ctx context.Context, ln net.Listener) {
defer ln.Close()
for {
select {
case <-ctx.Done():
return
default:
conn, err := ln.Accept()
if err != nil {
logrus.WithError(err).Error("Failed to accept connection")
} else {
go c.HandleConnection(ctx, conn)
}
}
}
}
func (c *connectorImpl) HandleConnection(ctx context.Context, frontendConn net.Conn) {
defer frontendConn.Close()
clientAddr := frontendConn.RemoteAddr()
logrus.WithFields(logrus.Fields{"clientAddr": clientAddr}).Info("Got connection")
inspectionBuffer := new(bytes.Buffer)
inspectionReader := io.TeeReader(frontendConn, inspectionBuffer)
packet, err := mcproto.ReadPacket(inspectionReader)
if err != nil {
logrus.WithError(err).WithField("clientAddr", clientAddr).Error("Failed to read packet")
return
}
logrus.WithFields(logrus.Fields{"length": packet.Length, "packetID": packet.PacketID}).Info("Got packet")
if packet.PacketID == mcproto.PacketIdHandshake {
handshake, err := mcproto.ReadHandshake(packet.Data)
if err != nil {
logrus.WithError(err).WithField("clientAddr", clientAddr).Error("Failed to read handshake")
return
}
logrus.WithFields(logrus.Fields{
"protocolVersion": handshake.ProtocolVersion,
"server": handshake.ServerAddress,
"serverPort": handshake.ServerPort,
"nextState": handshake.NextState,
}).Info("Got handshake")
backendHostPort := Routes.FindBackendForServerAddress(handshake.ServerAddress)
if backendHostPort == "" {
logrus.WithField("serverAddress", handshake.ServerAddress).Warn("Unable to find registered backend")
return
}
logrus.WithField("backendHostPort", backendHostPort).Info("Connecting to backend")
backendConn, err := net.Dial("tcp", backendHostPort)
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"serverAddress": handshake.ServerAddress,
"backend": backendHostPort,
}).Warn("Unable to connect to backend")
return
}
amount, err := io.Copy(backendConn, inspectionBuffer)
if err != nil {
logrus.WithError(err).Error("Failed to write handshake to backend connection")
return
}
logrus.WithField("amount", amount).Debug("Relayed handshake to backend")
pumpConnections(ctx, frontendConn, backendConn)
} else {
logrus.WithField("packetID", packet.PacketID).Error("Unexpected packetID, expected handshake")
}
}
func pumpConnections(ctx context.Context, frontendConn, backendConn net.Conn) {
defer backendConn.Close()
errors := make(chan error, 2)
go pumpFrames(backendConn, frontendConn, errors, "backend", "frontend")
go pumpFrames(frontendConn, backendConn, errors, "frontend", "backend")
for {
select {
case err := <-errors:
if err != io.EOF {
logrus.WithError(err).Error("Error observed on connection relay")
}
return
case <-ctx.Done():
return
}
}
}
func pumpFrames(incoming io.Reader, outgoing io.Writer, errors chan<- error, from, to string) {
amount, err := io.Copy(outgoing, incoming)
if err != nil {
errors <- err
}
logrus.WithField("amount", amount).Infof("Finished relay %s->%s", from, to)
}

131
server/routes.go Normal file
View File

@ -0,0 +1,131 @@
package server
import (
"sync"
"net/http"
"encoding/json"
"github.com/sirupsen/logrus"
"github.com/gorilla/mux"
)
func init() {
apiRoutes.Path("/routes").Methods("GET").
Headers("Accept", "application/json").
HandlerFunc(routesListHandler)
apiRoutes.Path("/routes").Methods("POST").
Headers("Content-Type", "application/json").
HandlerFunc(routesCreateHandler)
apiRoutes.Path("/routes/{serverAddress}").Methods("DELETE").HandlerFunc(routesDeleteHandler)
}
func routesListHandler(writer http.ResponseWriter, request *http.Request) {
mappings := Routes.GetMappings()
bytes, err := json.Marshal(mappings)
if err != nil {
logrus.WithError(err).Error("Failed to marshal mappings")
writer.WriteHeader(http.StatusInternalServerError)
return
}
writer.Write(bytes)
}
func routesDeleteHandler(writer http.ResponseWriter, request *http.Request) {
serverAddress := mux.Vars(request)["serverAddress"]
if serverAddress != "" {
if Routes.DeleteMapping(serverAddress) {
writer.WriteHeader(http.StatusOK)
} else {
writer.WriteHeader(http.StatusNotFound)
}
}
}
func routesCreateHandler(writer http.ResponseWriter, request *http.Request) {
var definition = struct {
ServerAddress string
Backend string
}{}
defer request.Body.Close()
decoder := json.NewDecoder(request.Body)
err := decoder.Decode(&definition)
if err != nil {
logrus.WithError(err).Error("Unable to get request body")
writer.WriteHeader(http.StatusBadRequest)
return
}
Routes.CreateMapping(definition.ServerAddress, definition.Backend)
writer.WriteHeader(http.StatusCreated)
}
type IRoutes interface {
RegisterAll(mappings map[string]string)
// FindBackendForServerAddress returns the host:port for the external server address, if registered.
// Otherwise, an empty string is returned
FindBackendForServerAddress(serverAddress string) string
GetMappings() map[string]string
DeleteMapping(serverAddress string) bool
CreateMapping(serverAddress string, backend string)
}
var Routes IRoutes = &routesImpl{}
func (r *routesImpl) RegisterAll(mappings map[string]string) {
r.Lock()
defer r.Unlock()
r.mappings = mappings
}
type routesImpl struct {
sync.RWMutex
mappings map[string]string
}
func (r *routesImpl) FindBackendForServerAddress(serverAddress string) string {
r.RLock()
defer r.RUnlock()
if r.mappings == nil {
return ""
} else {
return r.mappings[serverAddress]
}
}
func (r *routesImpl) GetMappings() map[string]string {
r.RLock()
defer r.RUnlock()
result := make(map[string]string, len(r.mappings))
for k, v := range r.mappings {
result[k] = v
}
return result
}
func (r *routesImpl) DeleteMapping(serverAddress string) bool {
r.Lock()
defer r.Unlock()
logrus.WithField("serverAddress", serverAddress).Info("Deleting route")
if _, ok := r.mappings[serverAddress]; ok {
delete(r.mappings, serverAddress)
return true
} else {
return false
}
}
func (r *routesImpl) CreateMapping(serverAddress string, backend string) {
r.Lock()
defer r.Unlock()
logrus.WithFields(logrus.Fields{
"serverAddress": serverAddress,
"backend": backend,
}).Info("Creating route")
r.mappings[serverAddress] = backend
}