Skip to content

Commit

Permalink
Merge pull request mas-bandwidth#196 from kbirk/matcher-sample-again
Browse files Browse the repository at this point in the history
Add the matcher sample back to the repo
  • Loading branch information
gafferongames authored Jan 20, 2024
2 parents 3e9259d + f733634 commit 407bb33
Show file tree
Hide file tree
Showing 5 changed files with 332 additions and 0 deletions.
28 changes: 28 additions & 0 deletions matcher/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM golang:1.20.13 AS matcher_build

# Matcher
WORKDIR /matcher

# Copy go.mod and go.sum files to the workspace separately and download dependecies.
# Doing this separately will cache these as its own separate layer
COPY ./go.mod .
COPY ./go.sum .
RUN go mod download

# Copy the source code as the last step
COPY . .

# Build the binary
RUN CGO_ENABLED=0 go build -o matcher.bin main.go

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

# Then we copy and run it from a slim image
FROM alpine:3.5
WORKDIR /matcher

COPY --from=matcher_build /matcher/matcher.bin .

EXPOSE 8081

ENTRYPOINT ["/matcher/matcher.bin"]
31 changes: 31 additions & 0 deletions matcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Yojimbo Matcher Sample

This is a sample matcher server written in go that will provide a connection token via the following endpoint:

```
GET /match/{protocolID}/{clientID}
```

# Building the Docker image:

To build the image run the following command from the `matcher` directory:

```sh
docker build --tag=matcher .
```

# Running the Docker container:

Run the container image mapping the port to your host machine:

```sh
docker run -d -p 8081:8081 --name matcher matcher
```

# Using the matcher:

To hit the container with a test request:

```sh
PROTOCOL_ID=123 && CLIENT_ID=42 && curl http://localhost:8081/match/${PROTOCOL_ID}/${CLIENT_ID}
```
11 changes: 11 additions & 0 deletions matcher/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/mas-bandwidth/matcher

go 1.20

require (
github.com/go-chi/chi/v5 v5.0.8
github.com/pkg/errors v0.9.1
golang.org/x/crypto v0.18.0
)

require golang.org/x/sys v0.16.0 // indirect
8 changes: 8 additions & 0 deletions matcher/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
254 changes: 254 additions & 0 deletions matcher/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package main

import (
"crypto/rand"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"strconv"
"time"

"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"golang.org/x/crypto/chacha20poly1305"
)

const (
port = 8081
serverAddress = "127.0.0.1"
serverPort = 40000
keyBytes = 32
authBytes = 16
connectTokenExpiry = 45
connectTokenBytes = 2048
connectTokenPrivateBytes = 1024
userDataBytes = 256
timeoutSeconds = 5
versionInfo = "NETCODE 1.02\x00"
verboseError = true
addressIPV4 = 1
addressIPV6 = 2
)

var (
stdoutLogger = log.New(os.Stdout, "yojimbo-matcher: ", log.Llongfile)
stderrLogger = log.New(os.Stderr, "yojimbo-matcher: ", log.Llongfile)
privateKey = []byte{
0x60, 0x6a, 0xbe, 0x6e, 0xc9, 0x19, 0x10, 0xea,
0x9a, 0x65, 0x62, 0xf6, 0x6f, 0x2b, 0x30, 0xe4,
0x43, 0x71, 0xd6, 0x2c, 0xd1, 0x99, 0x27, 0x26,
0x6b, 0x3c, 0x60, 0xf4, 0xb7, 0x15, 0xab, 0xa1,
}
)

func writeAddresses(buffer []byte, addresses []net.UDPAddr) int {
binary.LittleEndian.PutUint32(buffer[0:], (uint32)(len(addresses)))
offset := 4
for _, addr := range addresses {
ipv4 := addr.IP.To4()
port := addr.Port
if ipv4 != nil {
buffer[offset] = addressIPV4
buffer[offset+1] = ipv4[0]
buffer[offset+2] = ipv4[1]
buffer[offset+3] = ipv4[2]
buffer[offset+4] = ipv4[3]
buffer[offset+5] = (byte)(port & 0xFF)
buffer[offset+6] = (byte)(port >> 8)
} else {
buffer[offset] = addressIPV6
copy(buffer[offset+1:], addr.IP)
buffer[offset+17] = (byte)(port & 0xFF)
buffer[offset+18] = (byte)(port >> 8)
}
offset += 19
}
return offset
}

type connectTokenPrivate struct {
clientID uint64
TimeoutSeconds int32
ServerAddresses []net.UDPAddr
ClientToServerKey [keyBytes]byte
ServerToClientKey [keyBytes]byte
UserData [userDataBytes]byte
}

func newConnectTokenPrivate(clientID uint64, serverAddresses []net.UDPAddr, timeoutSeconds int32, userData []byte, clientToServerKey []byte, serverToClientKey []byte) *connectTokenPrivate {
connectTokenPrivate := &connectTokenPrivate{}
connectTokenPrivate.clientID = clientID
connectTokenPrivate.TimeoutSeconds = timeoutSeconds
connectTokenPrivate.ServerAddresses = serverAddresses
copy(connectTokenPrivate.UserData[:], userData)
copy(connectTokenPrivate.ClientToServerKey[:], clientToServerKey)
copy(connectTokenPrivate.ServerToClientKey[:], serverToClientKey)
return connectTokenPrivate
}

func (token *connectTokenPrivate) Write(buffer []byte) {
binary.LittleEndian.PutUint64(buffer[0:], token.clientID)
binary.LittleEndian.PutUint32(buffer[8:], (uint32)(token.TimeoutSeconds))
addressBytes := writeAddresses(buffer[12:], token.ServerAddresses)
copy(buffer[12+addressBytes:], token.ClientToServerKey[:])
copy(buffer[12+addressBytes+keyBytes:], token.ServerToClientKey[:])
copy(buffer[12+addressBytes+keyBytes*2:], token.UserData[:])
}

type connectToken struct {
protocolID uint64
CreateTimestamp uint64
ExpireTimestamp uint64
Sequence uint64
PrivateData *connectTokenPrivate
TimeoutSeconds int32
ServerAddresses []net.UDPAddr
ClientToServerKey [keyBytes]byte
ServerToClientKey [keyBytes]byte
PrivateKey [keyBytes]byte
}

func newConnectToken(clientID uint64, serverAddresses []net.UDPAddr, protocolID uint64, expireSeconds uint64, timeoutSeconds int32, userData []byte, privateKey []byte) (*connectToken, error) {
connectToken := &connectToken{}
connectToken.protocolID = protocolID
connectToken.CreateTimestamp = uint64(time.Now().Unix())
if expireSeconds >= 0 {
connectToken.ExpireTimestamp = connectToken.CreateTimestamp + expireSeconds
} else {
connectToken.ExpireTimestamp = 0xFFFFFFFFFFFFFFFF
}
connectToken.TimeoutSeconds = timeoutSeconds
connectToken.ServerAddresses = serverAddresses
err := fillWithRandomBytes(connectToken.ClientToServerKey[:])
if err != nil {
return nil, errors.Wrap(err, "failed to fill client to server key with random bytes")
}
err = fillWithRandomBytes(connectToken.ServerToClientKey[:])
if err != nil {
return nil, errors.Wrap(err, "failed to fill server to client key with random bytes")
}
copy(connectToken.PrivateKey[:], privateKey[:])
connectToken.PrivateData = newConnectTokenPrivate(clientID, serverAddresses, timeoutSeconds, userData, connectToken.ClientToServerKey[:], connectToken.ServerToClientKey[:])
return connectToken, nil
}

func fillWithRandomBytes(buf []byte) error {
_, err := rand.Read(buf)
if err != nil {
return err
}
return nil
}

func encryptAEAD(message []byte, additional []byte, nonce []byte, key []byte) error {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return errors.Wrap(err, "failed to create cipher")
}

// Encrypt the message and append the authentication tag.
aead.Seal(message[:0], nonce, message, additional)

return nil
}

func (token *connectToken) Write(buffer []byte) error {
copy(buffer, versionInfo)
binary.LittleEndian.PutUint64(buffer[13:], token.protocolID)
binary.LittleEndian.PutUint64(buffer[21:], token.CreateTimestamp)
binary.LittleEndian.PutUint64(buffer[29:], token.ExpireTimestamp)
nonce := make([]byte, 24)
err := fillWithRandomBytes(nonce)
if err != nil {
return errors.Wrap(err, "failed to fill nonce with random bytes")
}
copy(buffer[37:], nonce[:])
token.PrivateData.Write(buffer[61:])
additional := make([]byte, 13+8+8)
copy(additional, versionInfo[0:13])
binary.LittleEndian.PutUint64(additional[13:], token.protocolID)
binary.LittleEndian.PutUint64(additional[21:], token.ExpireTimestamp)
err = encryptAEAD(buffer[61:61+connectTokenPrivateBytes-authBytes], additional[:], nonce[:], token.PrivateKey[:])
if err != nil {
return errors.Wrap(err, "failed to encrypt message")
}
binary.LittleEndian.PutUint32(buffer[connectTokenPrivateBytes+61:], (uint32)(token.TimeoutSeconds))
offset := writeAddresses(buffer[1024+61+4:], token.ServerAddresses)
copy(buffer[1024+61+4+offset:], token.ClientToServerKey[:])
copy(buffer[1024+61+4+offset+keyBytes:], token.ServerToClientKey[:])
return nil
}

func generateConnectToken(clientID uint64, serverAddresses []net.UDPAddr, protocolID uint64, expireSeconds uint64, timeoutSeconds int32, userData []byte, privateKey []byte) ([]byte, error) {
connectToken, err := newConnectToken(clientID, serverAddresses, protocolID, expireSeconds, timeoutSeconds, userData, privateKey)
if err != nil {
return nil, errors.Wrap(err, "failed to create connect token")
}
buffer := make([]byte, connectTokenBytes)
err = connectToken.Write(buffer)
if err != nil {
return nil, errors.Wrap(err, "failed to serialize connect token")
}
return buffer, nil
}

func writeError(w http.ResponseWriter, err error, statusCode int) {
stderrLogger.Printf("%+v\n", err)
errMessage := "An error occured on the server while processing the request"
if verboseError {
errMessage = err.Error()
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(statusCode)
fmt.Fprint(w, errMessage)
}

func matchHandler(w http.ResponseWriter, r *http.Request) {

clientID, err := strconv.ParseUint(chi.URLParam(r, "clientID"), 10, 64)
if err != nil {
writeError(w, fmt.Errorf("Unable to parse clientID: %s", chi.URLParam(r, "clientID")), http.StatusBadRequest)
return
}
protocolID, err := strconv.ParseUint(chi.URLParam(r, "protocolID"), 10, 64)
if err != nil {
writeError(w, fmt.Errorf("Unable to parse protocolID: %s", chi.URLParam(r, "protocolID")), http.StatusBadRequest)
return
}

serverAddresses := make([]net.UDPAddr, 1)
serverAddresses[0] = net.UDPAddr{IP: net.ParseIP(serverAddress), Port: serverPort}

userData := make([]byte, userDataBytes)
connectToken, err := generateConnectToken(clientID, serverAddresses, protocolID, connectTokenExpiry, timeoutSeconds, userData, privateKey)
if err != nil {
writeError(w, errors.Wrap(err, "Failed to generate connect token"), http.StatusInternalServerError)
return
}
connectTokenBase64 := base64.StdEncoding.EncodeToString(connectToken)
w.Header().Set("Content-Type", "application/text")
_, err = io.WriteString(w, connectTokenBase64)
if err != nil {
writeError(w, errors.Wrap(err, "Failed to write response"), http.StatusInternalServerError)
return
}
stderrLogger.Printf("Matched client %.16x to %s:%d\n", clientID, serverAddress, serverPort)
}

func main() {
stderrLogger.Printf("Started matchmaker on port %d\n", port)

router := chi.NewRouter()
router.Get("/match/{protocolID:[0-9]+}/{clientID:[0-9]+}", matchHandler)

err := http.ListenAndServe(":"+strconv.Itoa(port), router)
if err != nil {
stderrLogger.Fatalf("%+v\n", err)
}
}

0 comments on commit 407bb33

Please sign in to comment.