├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question-template.md └── workflows │ ├── build.yml │ ├── release.yml │ └── unit-test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── basic.go ├── basic_test.go ├── cmd ├── Ultraviolet │ ├── .goreleaser.yml │ └── main.go └── main.go ├── config ├── file.go ├── file_reader.go ├── file_reader_test.go ├── file_test.go ├── types.go ├── verify.go └── verify_test.go ├── core ├── errors.go ├── proxy.go ├── request_data.go ├── server.go ├── server_catalog.go └── serverstate_string.go ├── examples ├── minimal.server.json ├── server-example.json ├── ultraviolet-dashboard.json ├── ultraviolet.json └── ultraviolet.service.example ├── go.mod ├── go.sum ├── mc ├── conn.go ├── handshakestate_string.go ├── packet.go ├── packet_login.go ├── packet_login_test.go ├── packet_status.go ├── packet_status_test.go ├── packet_test.go ├── type.go └── type_test.go ├── module ├── conn_creator.go ├── conn_creator_test.go ├── conn_limiter.go ├── conn_limiter_test.go ├── handshake_modifier.go ├── handshake_modifier_test.go ├── state_agent.go ├── state_agent_test.go ├── status_cache.go └── status_cache_test.go ├── proxy.go ├── proxy_test.go ├── script ├── build ├── run └── test ├── server.go ├── src ├── config.go ├── config_test.go ├── packet.go ├── packet_test.go ├── server.go ├── server_test.go └── type.go └── worker ├── api.go ├── backend.go ├── backend_manager.go ├── backend_manager_test.go ├── backend_test.go ├── backendaction_string.go ├── proxyaction_string.go ├── run.go ├── type.go ├── worker.go ├── worker_manager.go ├── worker_manager_test.go └── worker_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[WAIT WHAT -- I didnt expect this behavior]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **System** 14 | System version and general specs (ram, cpu/core count) UV was running on. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[PLS IMPLEMENT THIS FOR ME]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question template 3 | about: I have a question about Ultraviolet 4 | title: "[LE QUESTION]" 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | When you're about to ask something make sure: 11 | - Its a Ultraviolet related question and not something which has to do with docker, linux or something else 12 | - Did you check the wiki or the readme.md or it has already been answered in there? 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Set up Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: 1.16 16 | 17 | - name: Build 18 | run: go build ./cmd/Ultraviolet/ 19 | env: 20 | GOARCH: amd64 21 | GOOS: linux 22 | 23 | - name: Save artifact 24 | uses: actions/upload-artifact@v2 25 | with: 26 | name: ultraviolet_linux_amd64 27 | path: ./Ultraviolet 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - 17 | name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.16 21 | - 22 | name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v2 24 | with: 25 | version: latest 26 | args: release 27 | workdir: ./cmd/Ultraviolet 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | docker-release: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - 34 | name: Checkout 35 | uses: actions/checkout@v2 36 | - 37 | name: Set env 38 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 39 | - 40 | name: Set up QEMU 41 | uses: docker/setup-qemu-action@v1 42 | - 43 | name: Set up Docker Buildx 44 | uses: docker/setup-buildx-action@v1 45 | - 46 | name: Log in to Docker Hub 47 | uses: docker/login-action@v1 48 | with: 49 | username: ${{ secrets.DOCKER_USERNAME }} 50 | password: ${{ secrets.DOCKER_PASSWORD }} 51 | - 52 | name: Login to GitHub Container Registry 53 | uses: docker/login-action@v1 54 | with: 55 | registry: ghcr.io 56 | username: ${{ github.repository_owner }} 57 | password: ${{ secrets.GITHUB_TOKEN }} 58 | - 59 | name: Build and push 60 | uses: docker/build-push-action@v2 61 | with: 62 | context: . 63 | platforms: linux/amd64,linux/arm64 64 | push: true 65 | tags: | 66 | realdragonium/ultraviolet:latest 67 | realdragonium/ultraviolet:${{ env.RELEASE_VERSION }} 68 | ghcr.io/realdragonium/ultraviolet:latest 69 | ghcr.io/realdragonium/ultraviolet:${{ env.RELEASE_VERSION }} 70 | 71 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.16 21 | 22 | - name: Test 23 | run: go test ./... 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | *.profile 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | # goreleaser 19 | dist/ 20 | 21 | ultraviolet 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16.0-buster AS builder 2 | LABEL stage=intermediate 3 | WORKDIR /build 4 | COPY . . 5 | ENV GO111MODULE=on 6 | ENV CGO_ENABLED=0 7 | ENV GOOS=linux 8 | ENV GOARCH=amd64 9 | RUN go build -ldflags '-X "github.com/realDragonium/Ultraviolet/cmd.uvVersion=docker"' ./cmd/Ultraviolet/ 10 | 11 | FROM scratch 12 | WORKDIR / 13 | COPY --from=builder /build/Ultraviolet ./ultraviolet 14 | ENTRYPOINT [ "./ultraviolet", "run" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dragonium 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ultraviolet 2 | Join the Infrared discord! 3 | [![Discord](https://img.shields.io/discord/800456341088370698?label=discord&logo=discord)](https://discord.gg/r98YPRsZAx) 4 | 5 | ## What is Ultraviolet? 6 | Its a reverse minecraft proxy, capable of serving as a placeholder when the server is offline for status response to clients. It also has some basic anti backend ddos features. So can it ask players to verify themselves when there are to many players trying to join within a given timeframe and it will (by default) cache the status of the server. 7 | 8 | 9 | ## Extra Features 10 | [x] Proxy Protocol(v2) support 11 | [x] RealIP (v2.4&v2.5) 12 | [x] Rate limiting -> Login verification 13 | [x] Status caching (online status only) 14 | [x] Offline status placeholder 15 | [x] Prometheus Support 16 | [x] API (Reload server config files, more later) 17 | 18 | 19 | ## How to run 20 | Ultraviolet will, when no config is specified by the command, use `/etc/ultraviolet` as work dir and create here an `ultraviolet.json` file for you. 21 | ``` 22 | $ ./ultraviolet run 23 | ``` 24 | 25 | ## How to build 26 | Ultraviolet can be ran by using docker or you can also build a binary yourself by running: 27 | ``` 28 | $ cd cmd/Ultraviolet/ 29 | $ go build 30 | ``` 31 | 32 | ## How to run with docker 33 | You can run ultraviolet in docker by executing the follow command: 34 | ``` 35 | docker run -d -p 25565:25565 -v /etc/ultraviolet:/etc/ultraviolet --restart=unless-stopped realdragonium/ultraviolet:latest 36 | ``` 37 | You could also pull it from github packages with 38 | ``` 39 | docker pull ghcr.io/realdragonium/ultraviolet:latest 40 | ``` 41 | Or you can make one yourself with the `Dockerfile` in the root folder of the project. 42 | 43 | ## Some notes 44 | ### Limited connections when running binary 45 | Because linux the default settings for fd is 1024, this means that you can by default Ultraviolet can have 1024 open connections before it starts refusing connections because it cant open anymore fds. Because of some internal queues you should consider increasing the limit if you expect to proxy over 900 open connections at the same time. 46 | 47 | ### File examples 48 | In the folder `examples` in the project are a few related file examples which can be used together with Ultraviolet. There is also a basic grafana dashboard layout there which can be used together with the prometheus feature. 49 | 50 | ### Hotswapping to newer versions 51 | Its not necessary to use this to reload the server configs, there is also an api or an command if you want to reload the server configs. 52 | 53 | This has implemented [tableflip](https://github.com/cloudflare/tableflip) which should make it able to reload/hotswap Ultraviolet without closing existing connections on Linux and macOS. Ultraviolet should still be usable on windows (testing purposes only pls). 54 | Check their [documentation](https://pkg.go.dev/github.com/cloudflare/tableflip) to know what or how. 55 | 56 | IMPORTANT (when using this feature): There is a limit of one 'parent' process. So when you reload Ultraviolet once you need to wait until the parent process is closed (all previous connections have been closed) before you can reload it again. 57 | 58 | ## Command-Line 59 | The follows commands can be used with ultraviolet, all flags (if related) should work for every command and be used by every command if you used it for one command. 60 | 61 | So far it only can use: 62 | - run 63 | - reload 64 | 65 | ### Flags 66 | `-config` specifies the path to the config directory [default: `"/etc/ultraviolet/"`] (if you want to use this directory you dont have to use this flag.) 67 | 68 | 69 | # Config 70 | Check the [wiki](https://github.com/realDragonium/Ultraviolet/wiki/Config) for more information about the config. 71 | 72 | There are also actual file which can be used as examples in the example folder. -------------------------------------------------------------------------------- /basic.go: -------------------------------------------------------------------------------- 1 | package ultraviolet 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/realDragonium/Ultraviolet/core" 12 | "github.com/realDragonium/Ultraviolet/mc" 13 | ) 14 | 15 | var ( 16 | ConnTimeoutDuration = 5 * time.Second 17 | ) 18 | 19 | type API interface { 20 | Run(addr string) 21 | Close() 22 | } 23 | 24 | func ProcessRequest(reqData core.RequestData, servers core.ServerCatalog) (action core.ServerAction, pk mc.Packet, err error) { 25 | server, err := LookupServer(reqData, servers) 26 | if err != nil { 27 | if errors.Is(err, core.ErrNoServerFound) && reqData.Type == mc.Status { 28 | pk = servers.DefaultStatus() 29 | } 30 | return 31 | } 32 | 33 | action = server.ConnAction(reqData) 34 | 35 | switch action { 36 | case core.VERIFY_CONN: 37 | pk = servers.VerifyConn() 38 | case core.STATUS: 39 | pk = server.Status() 40 | } 41 | 42 | return 43 | } 44 | 45 | func ReadStuff(conn net.Conn) (reqData core.RequestData, err error) { 46 | conn.SetDeadline(time.Now().Add(ConnTimeoutDuration)) 47 | mcConn := mc.NewMcConn(conn) 48 | 49 | handshakePacket, err := mcConn.ReadPacket() 50 | if errors.Is(err, os.ErrDeadlineExceeded) { 51 | err = core.ErrClientToSlow 52 | return 53 | } else if err != nil { 54 | return 55 | } 56 | 57 | handshake, err := mc.UnmarshalServerBoundHandshake(handshakePacket) 58 | if err != nil { 59 | log.Printf("error while parsing handshake: %v", err) 60 | } 61 | reqType := mc.RequestState(handshake.NextState) 62 | if reqType == mc.UnknownState { 63 | err = core.ErrNotValidHandshake 64 | return 65 | } 66 | 67 | packet, err := mcConn.ReadPacket() 68 | if errors.Is(err, os.ErrDeadlineExceeded) { 69 | err = core.ErrClientToSlow 70 | return 71 | } else if err != nil { 72 | return 73 | } 74 | conn.SetDeadline(time.Time{}) 75 | 76 | serverAddr := strings.ToLower(handshake.ParseServerAddress()) 77 | reqData = core.RequestData{ 78 | Type: reqType, 79 | ServerAddr: serverAddr, 80 | Addr: conn.RemoteAddr(), 81 | Handshake: handshake, 82 | } 83 | 84 | if reqType == mc.Login { 85 | loginStart, err := mc.UnmarshalServerBoundLoginStart(packet) 86 | if err != nil { 87 | log.Printf("error while parsing login packet: %v", err) 88 | return reqData, err 89 | } 90 | reqData.Username = string(loginStart.Name) 91 | } 92 | 93 | return reqData, nil 94 | } 95 | 96 | func LookupServer(req core.RequestData, servers core.ServerCatalog) (core.Server, error) { 97 | return servers.Find(req.ServerAddr) 98 | } 99 | 100 | func SendResponse(conn net.Conn, pk mc.Packet, withPing bool) error { 101 | conn.SetDeadline(time.Now().Add(ConnTimeoutDuration)) 102 | 103 | mcConn := mc.NewMcConn(conn) 104 | 105 | if err := mcConn.WritePacket(pk); err != nil { 106 | return err 107 | } 108 | 109 | if withPing { 110 | pingPacket, err := mcConn.ReadPacket() 111 | if err != nil { 112 | return err 113 | } 114 | 115 | mcConn.WritePacket(pingPacket) 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func FullRun(conn net.Conn, servers core.ServerCatalog) error { 122 | reqData, err := ReadStuff(conn) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | server, err := LookupServer(reqData, servers) 128 | if errors.Is(err, core.ErrNoServerFound) && reqData.Type == mc.Status { 129 | return SendResponse(conn, servers.DefaultStatus(), true) 130 | } else if err != nil { 131 | log.Printf("got error: %v", err) 132 | return conn.Close() 133 | } 134 | 135 | return ProcessServer(conn, server, reqData) 136 | } 137 | 138 | func ProcessServer(conn net.Conn, server core.Server, reqData core.RequestData) error { 139 | action := server.ConnAction(reqData) 140 | 141 | if action == core.PROXY { 142 | go ProxyConnection(conn, server, reqData) 143 | return nil 144 | } 145 | 146 | defer conn.Close() 147 | 148 | var responsePk mc.Packet 149 | switch action { 150 | // TOOD: Figure this one out 151 | // case core.VERIFY_CONN: 152 | // responsePk = servers.VerifyConn() 153 | case core.STATUS: 154 | responsePk = server.Status() 155 | case core.CLOSE: 156 | return nil 157 | } 158 | 159 | return SendResponse(conn, responsePk, action == core.STATUS) 160 | } 161 | 162 | func ProxyConnection(client net.Conn, server core.Server, reqData core.RequestData) (err error) { 163 | serverConn, err := server.CreateConn(reqData) 164 | if err != nil { 165 | return 166 | } 167 | 168 | go func() { 169 | pipe(serverConn, client) 170 | client.Close() 171 | }() 172 | 173 | go func() { 174 | pipe(client, serverConn) 175 | serverConn.Close() 176 | }() 177 | 178 | return 179 | } 180 | 181 | func pipe(c1, c2 net.Conn) { 182 | buffer := make([]byte, 0xffff) 183 | for { 184 | n, err := c1.Read(buffer) 185 | if err != nil { 186 | return 187 | } 188 | _, err = c2.Write(buffer[:n]) 189 | if err != nil { 190 | return 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /cmd/Ultraviolet/.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - go generate ./... 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | goarch: 13 | - amd64 14 | - arm 15 | - arm64 16 | ldflags: 17 | - -X github.com/realDragonium/Ultraviolet/cmd.uvVersion={{.Version}} 18 | release: 19 | draft: true 20 | name_template: '{{.Version}}' 21 | checksum: 22 | name_template: 'checksums.txt' 23 | snapshot: 24 | name_template: '{{ incpatch .Tag }}-next' 25 | archives: 26 | - name_template: 'ultraviolet-{{.Os}}-{{.Arch}}-{{.Version}}' 27 | changelog: 28 | sort: asc 29 | filters: 30 | exclude: 31 | - '^docs:' 32 | - '^test:' 33 | - '^ignore:' 34 | -------------------------------------------------------------------------------- /cmd/Ultraviolet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ultravioletcmd "github.com/realDragonium/Ultraviolet/cmd" 4 | 5 | func main() { 6 | ultravioletcmd.Main() 7 | } 8 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "path/filepath" 14 | "runtime" 15 | "syscall" 16 | 17 | "github.com/cloudflare/tableflip" 18 | "github.com/pires/go-proxyproto" 19 | ultraviolet "github.com/realDragonium/Ultraviolet" 20 | "github.com/realDragonium/Ultraviolet/config" 21 | "github.com/realDragonium/Ultraviolet/core" 22 | ultravioletv2 "github.com/realDragonium/Ultraviolet/src" 23 | "github.com/realDragonium/Ultraviolet/worker" 24 | ) 25 | 26 | var ( 27 | defaultCfgPath = "/etc/ultraviolet" 28 | configPath string 29 | uvVersion = "(unknown version)" 30 | pidFilePath = "/bin/ultraviolet/uv.pid" 31 | pidFileName = "uv.pid" 32 | upg *tableflip.Upgrader 33 | ) 34 | 35 | func Main() { 36 | if len(os.Args) < 2 { 37 | log.Fatalf("Didnt receive enough arguments, try adding 'run' or 'reload' after the command") 38 | } 39 | 40 | flags := flag.NewFlagSet("", flag.ExitOnError) 41 | cfgDir := flags.String("config", defaultCfgPath, "`Path` to be used as directory") 42 | flags.Parse(os.Args[2:]) 43 | 44 | configPath = *cfgDir 45 | 46 | switch os.Args[1] { 47 | case "run": 48 | log.Printf("Starting Ultraviolet %v", uvVersion) 49 | err := runProxy(configPath) 50 | if err != nil { 51 | log.Printf("got error while starting up: %v", err) 52 | } 53 | case "reload": 54 | err := callReloadAPI(configPath) 55 | if err != nil { 56 | log.Fatalf("got error: %v ", err) 57 | } 58 | log.Println("Finished reloading") 59 | case "run-experimental": 60 | err := ultravioletv2.Run(configPath) 61 | if err != nil { 62 | log.Fatalf("Error: %v", err) 63 | } 64 | } 65 | } 66 | 67 | func runProxy(configPath string) error { 68 | uvReader := config.NewUVConfigFileReader(configPath) 69 | 70 | cfg, err := uvReader() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | var newProxyFunc core.NewProxyFunc 76 | newProxyFunc = worker.NewProxy 77 | if cfg.UseLessStableMode { 78 | newProxyFunc = ultraviolet.NewProxy 79 | } 80 | 81 | return RunProxy(configPath, uvVersion, newProxyFunc) 82 | } 83 | 84 | func RunProxy(configPath, version string, newProxy core.NewProxyFunc) error { 85 | pidFilePath = filepath.Join(configPath, pidFileName) 86 | uvReader := config.NewUVConfigFileReader(configPath) 87 | serverCfgReader := config.NewBackendConfigFileReader(configPath, config.VerifyConfigs) 88 | cfg, err := uvReader() 89 | if err != nil { 90 | return err 91 | } 92 | 93 | notUseHotSwap := !cfg.UseTableflip || runtime.GOOS == "windows" || version == "docker" 94 | newListener, err := createListener(cfg, notUseHotSwap) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | proxy := newProxy(uvReader, newListener, serverCfgReader.Read) 100 | err = proxy.Start() 101 | if err != nil { 102 | return err 103 | } 104 | 105 | if notUseHotSwap { 106 | select {} 107 | } 108 | 109 | if err := upg.Ready(); err != nil { 110 | panic(err) 111 | } 112 | <-upg.Exit() 113 | 114 | // log.Println("Waiting for all connections to be closed before shutting down") 115 | // for { 116 | // active := backendManager.CheckActiveConnections() 117 | // if !active { 118 | // break 119 | // } 120 | // time.Sleep(time.Minute) 121 | // } 122 | // log.Println("All connections closed, shutting down process") 123 | return nil 124 | } 125 | 126 | func createListener(cfg config.UltravioletConfig, notUseHotSwap bool) (net.Listener, error) { 127 | var ln net.Listener 128 | var err error 129 | if notUseHotSwap { 130 | ln, err = net.Listen("tcp", cfg.ListenTo) 131 | } else { 132 | if cfg.PidFile == "" { 133 | cfg.PidFile = pidFilePath 134 | } 135 | if _, err := os.Stat(cfg.PidFile); errors.Is(err, os.ErrNotExist) { 136 | pid := fmt.Sprint(os.Getpid()) 137 | bb := []byte(pid) 138 | os.WriteFile(cfg.PidFile, bb, os.ModePerm) 139 | } 140 | ln, err = tableflipListener(cfg) 141 | } 142 | 143 | if err != nil { 144 | log.Fatalf("Can't listen: %v", err) 145 | } 146 | 147 | if cfg.AcceptProxyProtocol { 148 | policyFunc := func(upstream net.Addr) (proxyproto.Policy, error) { 149 | return proxyproto.REQUIRE, nil 150 | } 151 | proxyListener := &proxyproto.Listener{ 152 | Listener: ln, 153 | Policy: policyFunc, 154 | } 155 | return proxyListener, nil 156 | } 157 | return ln, nil 158 | } 159 | 160 | func tableflipListener(cfg config.UltravioletConfig) (net.Listener, error) { 161 | var err error 162 | upg, err = tableflip.New(tableflip.Options{ 163 | PIDFile: cfg.PidFile, 164 | }) 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | go func() { 169 | sig := make(chan os.Signal, 1) 170 | signal.Notify(sig, syscall.SIGHUP) 171 | for range sig { 172 | err := upg.Upgrade() 173 | if err != nil { 174 | log.Println("upgrade failed:", err) 175 | } 176 | } 177 | }() 178 | return upg.Listen("tcp", cfg.ListenTo) 179 | } 180 | 181 | func callReloadAPI(configPath string) error { 182 | mainCfg, err := config.ReadUltravioletConfig(configPath) 183 | if err != nil { 184 | log.Fatalf("Read main config file error: %v", err) 185 | } 186 | url := fmt.Sprintf("http://%s/reload", mainCfg.APIBind) 187 | resp, err := http.Get(url) 188 | if err != nil { 189 | return err 190 | } 191 | bb, err := ioutil.ReadAll(resp.Body) 192 | if err != nil { 193 | return err 194 | } 195 | fmt.Printf("%s", bb) 196 | resp.Body.Close() 197 | return nil 198 | } 199 | -------------------------------------------------------------------------------- /config/file.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/x509" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | "path/filepath" 15 | "time" 16 | 17 | "github.com/realDragonium/Ultraviolet/mc" 18 | ) 19 | 20 | var ( 21 | ErrPrivateKey = errors.New("could not load private key") 22 | ErrCantCombineConfigs = errors.New("failed to combine config structs") 23 | ErrFailedToConvertConfig = errors.New("failed to convert server config to a more usable config") 24 | 25 | MainConfigFileName = "ultraviolet.json" 26 | ) 27 | 28 | func ReadUltravioletConfig(path string) (UltravioletConfig, error) { 29 | cfg := DefaultUltravioletConfig() 30 | filePath := filepath.Join(path, MainConfigFileName) 31 | 32 | if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) { 33 | if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { 34 | err := os.MkdirAll(path, os.ModePerm) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | } 39 | bb, err := json.Marshal(cfg) 40 | if err != nil { 41 | return UltravioletConfig{}, err 42 | } 43 | err = os.WriteFile(filePath, bb, os.ModePerm) 44 | if err != nil { 45 | return cfg, err 46 | } 47 | return cfg, nil 48 | } 49 | 50 | bb, err := os.ReadFile(filePath) 51 | if err != nil { 52 | return UltravioletConfig{}, err 53 | } 54 | if err := json.Unmarshal(bb, &cfg); err != nil { 55 | return cfg, err 56 | } 57 | return cfg, nil 58 | } 59 | 60 | func CombineUltravioletConfigs(old, new UltravioletConfig) (UltravioletConfig, error) { 61 | cfg := old 62 | bb, err := json.Marshal(new) 63 | if err != nil { 64 | return cfg, ErrCantCombineConfigs 65 | } 66 | if err := json.Unmarshal(bb, &cfg); err != nil { 67 | return cfg, ErrCantCombineConfigs 68 | } 69 | return cfg, nil 70 | } 71 | 72 | func ReadPrivateKey(path string) (*ecdsa.PrivateKey, error) { 73 | var key *ecdsa.PrivateKey 74 | bb, err := ioutil.ReadFile(path) 75 | if err != nil { 76 | return key, err 77 | } 78 | return x509.ParseECPrivateKey(bb) 79 | } 80 | 81 | func CheckExistingGeneratedKey(cfg ServerConfig) (*ecdsa.PrivateKey, bool) { 82 | dir := filepath.Dir(cfg.FilePath) 83 | privkeyFileName := filepath.Join(dir, fmt.Sprintf("%s-%s", cfg.Domains[0], "private.key")) 84 | if _, err := os.Stat(privkeyFileName); err != nil { 85 | if os.IsNotExist(err) { 86 | return nil, false 87 | } 88 | } 89 | privateKey, err := ReadPrivateKey(privkeyFileName) 90 | if err != nil { 91 | log.Printf("error during reading key: %v", err) 92 | return nil, false 93 | } 94 | return privateKey, true 95 | } 96 | 97 | func GenerateKeys(cfg ServerConfig) *ecdsa.PrivateKey { 98 | privkey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) 99 | if err != nil { 100 | log.Printf("error during creating privatekey: %v", err) 101 | return privkey 102 | } 103 | pubkey := privkey.Public() 104 | dir := filepath.Dir(cfg.FilePath) 105 | privkeyFileName := filepath.Join(dir, fmt.Sprintf("%s-%s", cfg.Domains[0], "private.key")) 106 | pubkeyFileName := filepath.Join(dir, fmt.Sprintf("%s-%s", cfg.Domains[0], "public.key")) 107 | 108 | privkeyFile, err := os.Create(privkeyFileName) 109 | if err != nil { 110 | log.Printf("error during creating private key file: %v", err) 111 | } 112 | privkeyBytes, err := x509.MarshalECPrivateKey(privkey) 113 | if err != nil { 114 | log.Printf("error during marshal private key: %v", err) 115 | } 116 | if _, err := privkeyFile.Write(privkeyBytes); err != nil { 117 | log.Printf("error during saving private key to file: %v", err) 118 | } 119 | if err := privkeyFile.Close(); err != nil { 120 | log.Printf("error during closing private key file: %v", err) 121 | } 122 | 123 | pubkeyFile, err := os.Create(pubkeyFileName) 124 | if err != nil { 125 | log.Printf("error during creating public key file: %v", err) 126 | } 127 | pubkeyBytes, err := x509.MarshalPKIXPublicKey(pubkey) 128 | if err != nil { 129 | log.Printf("error during marshal public key: %v", err) 130 | } 131 | if _, err := pubkeyFile.Write(pubkeyBytes); err != nil { 132 | log.Printf("error during saving public key to file: %v", err) 133 | } 134 | if err := pubkeyFile.Close(); err != nil { 135 | log.Printf("error during closing public key file: %v", err) 136 | } 137 | return privkey 138 | } 139 | 140 | func ServerToAPIConfig(cfg ServerConfig) (APIServerConfig, error) { 141 | apiCfg := APIServerConfig{ 142 | Domains: cfg.Domains, 143 | ProxyTo: cfg.ProxyTo, 144 | ProxyBind: cfg.ProxyBind, 145 | DialTimeout: cfg.DialTimeout, 146 | SendProxyProtocol: cfg.SendProxyProtocol, 147 | IsOnline: true, 148 | UseStatusCache: false, 149 | CachedStatus: mc.SimpleStatus{}, 150 | DisconnectMessage: cfg.DisconnectMessage, 151 | } 152 | 153 | return apiCfg, nil 154 | } 155 | 156 | 157 | func ServerToBackendConfig(cfg ServerConfig) (BackendWorkerConfig, error) { 158 | name := cfg.Name 159 | if name == "" { 160 | name = cfg.Domains[0] 161 | } 162 | workerCfg := BackendWorkerConfig{ 163 | Name: name, 164 | ProxyTo: cfg.ProxyTo, 165 | ProxyBind: cfg.ProxyBind, 166 | SendProxyProtocol: cfg.SendProxyProtocol, 167 | RateLimit: cfg.RateLimit, 168 | OldRealIp: cfg.OldRealIP, 169 | NewRealIP: cfg.NewRealIP, 170 | StateOption: NewStateOption(cfg.CheckStateOption), 171 | } 172 | 173 | if cfg.NewRealIP { 174 | var privateKey *ecdsa.PrivateKey 175 | var err error 176 | privateKey, err = ReadPrivateKey(cfg.RealIPKey) 177 | if errors.Is(err, os.ErrNotExist) { 178 | if key, ok := CheckExistingGeneratedKey(cfg); ok { 179 | privateKey = key 180 | } else { 181 | log.Printf("No existing key for %s has been found, generating one...", cfg.ID()) 182 | privateKey = GenerateKeys(cfg) 183 | } 184 | } else if err != nil { 185 | return BackendWorkerConfig{}, err 186 | } 187 | workerCfg.NewRealIP = true 188 | workerCfg.RealIPKey = privateKey 189 | } 190 | disconPk := mc.ClientBoundDisconnect{ 191 | Reason: mc.Chat(cfg.DisconnectMessage), 192 | }.Marshal() 193 | workerCfg.DisconnectPacket = disconPk 194 | 195 | offlineStatusPk := cfg.OfflineStatus.Marshal() 196 | workerCfg.OfflineStatus = offlineStatusPk 197 | 198 | stateUpdateCooldown, err := time.ParseDuration(cfg.StateUpdateCooldown) 199 | if err != nil { 200 | stateUpdateCooldown = time.Second 201 | } 202 | workerCfg.StateUpdateCooldown = stateUpdateCooldown 203 | 204 | dialTimeout, err := time.ParseDuration(cfg.DialTimeout) 205 | if err != nil { 206 | dialTimeout = time.Second 207 | } 208 | workerCfg.DialTimeout = dialTimeout 209 | 210 | if cfg.CacheStatus { 211 | cacheCooldown, err := time.ParseDuration(cfg.CacheUpdateCooldown) 212 | if err != nil { 213 | cacheCooldown = time.Second 214 | } 215 | workerCfg.CacheStatus = true 216 | workerCfg.CacheUpdateCooldown = cacheCooldown 217 | workerCfg.ValidProtocol = cfg.ValidProtocol 218 | } 219 | 220 | if cfg.RateLimit > 0 { 221 | rateDuration, err := time.ParseDuration(cfg.RateDuration) 222 | if err != nil { 223 | rateDuration = time.Second 224 | } 225 | rateBanCooldown, err := time.ParseDuration(cfg.RateBanListCooldown) 226 | if err != nil { 227 | rateBanCooldown = 15 * time.Minute 228 | } 229 | rateDisconPk := mc.ClientBoundDisconnect{ 230 | Reason: mc.String(cfg.RateDisconMsg), 231 | }.Marshal() 232 | 233 | workerCfg.RateLimitDuration = rateDuration 234 | workerCfg.RateBanListCooldown = rateBanCooldown 235 | workerCfg.RateDisconPk = rateDisconPk 236 | } 237 | return workerCfg, nil 238 | } 239 | 240 | func CombineServerConfigs(old, new ServerConfig) (ServerConfig, error) { 241 | cfg := old 242 | bb, err := json.Marshal(new) 243 | if err != nil { 244 | return cfg, ErrCantCombineConfigs 245 | } 246 | if err := json.Unmarshal(bb, &cfg); err != nil { 247 | return cfg, ErrCantCombineConfigs 248 | } 249 | return cfg, nil 250 | } 251 | -------------------------------------------------------------------------------- /config/file_reader.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | var ErrNoConfigFiles = errors.New("no config files found") 12 | 13 | func NewBackendConfigFileReader(path string, verifier VerifyFunc) backendConfigFileReader { 14 | return backendConfigFileReader{ 15 | path: path, 16 | verifier: verifier, 17 | } 18 | } 19 | 20 | type backendConfigFileReader struct { 21 | path string 22 | verifier VerifyFunc 23 | } 24 | 25 | func (reader backendConfigFileReader) Read() ([]ServerConfig, error) { 26 | cfgs, err := ReadServerConfigs(reader.path) 27 | if err != nil { 28 | return nil, err 29 | } 30 | err = reader.verifier(cfgs) 31 | if err != nil { 32 | return cfgs, err 33 | } 34 | return cfgs, nil 35 | } 36 | 37 | func ReadServerConfigs(path string) ([]ServerConfig, error) { 38 | var cfgs []ServerConfig 39 | var filePaths []string 40 | err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 41 | if err != nil { 42 | return err 43 | } 44 | if info.IsDir() { 45 | return nil 46 | } 47 | if filepath.Ext(path) != ".json" { 48 | return nil 49 | } 50 | if info.Name() == MainConfigFileName { 51 | return nil 52 | } 53 | filePaths = append(filePaths, path) 54 | return nil 55 | }) 56 | if len(filePaths) == 0 { 57 | return cfgs, ErrNoConfigFiles 58 | } 59 | if err != nil { 60 | return cfgs, err 61 | } 62 | for _, filePath := range filePaths { 63 | cfg, err := LoadServerCfgFromPath(filePath) 64 | if err != nil { 65 | return nil, err 66 | } 67 | cfgs = append(cfgs, cfg) 68 | } 69 | return cfgs, nil 70 | } 71 | 72 | func LoadServerCfgFromPath(path string) (ServerConfig, error) { 73 | bb, err := ioutil.ReadFile(path) 74 | if err != nil { 75 | return ServerConfig{}, err 76 | } 77 | cfg := DefaultServerConfig() 78 | if err := json.Unmarshal(bb, &cfg); err != nil { 79 | return cfg, err 80 | } 81 | cfg.FilePath = path 82 | return cfg, nil 83 | } 84 | 85 | func NewIVConfigFileReader(path string) UVConfigReader { 86 | return uvConfigFileReader{ 87 | path: path, 88 | }.Read 89 | } 90 | 91 | func NewUVConfigFileReader(path string) UVConfigReader { 92 | return uvConfigFileReader{ 93 | path: path, 94 | }.Read 95 | } 96 | 97 | type uvConfigFileReader struct { 98 | path string 99 | } 100 | 101 | func (reader uvConfigFileReader) Read() (UltravioletConfig, error) { 102 | return ReadUltravioletConfig(reader.path) 103 | } 104 | 105 | func NewUVReader(cfg UltravioletConfig) func()(UltravioletConfig, error) { 106 | return func() (UltravioletConfig, error) { 107 | return cfg, nil 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /config/file_reader_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/realDragonium/Ultraviolet/config" 14 | "github.com/realDragonium/Ultraviolet/mc" 15 | ) 16 | 17 | var emptyVerifyFunc = func(cfgs []config.ServerConfig) error { 18 | return nil 19 | } 20 | 21 | func writeDefaultMainConfig(path string) { 22 | cfg := config.UltravioletConfig{ 23 | ListenTo: ":25565", 24 | DefaultStatus: mc.SimpleStatus{ 25 | Name: "Ultraviolet", 26 | Protocol: 755, 27 | Description: "One dangerous proxy", 28 | }, 29 | NumberOfWorkers: 5, 30 | } 31 | file, _ := json.MarshalIndent(cfg, "", " ") 32 | filename := filepath.Join(path, config.MainConfigFileName) 33 | os.WriteFile(filename, file, os.ModePerm) 34 | } 35 | 36 | func writeServerConfig(path string, cfg config.ServerConfig) error { 37 | file, err := json.MarshalIndent(cfg, "", " ") 38 | if err != nil { 39 | return err 40 | } 41 | tmpfile, err := ioutil.TempFile(path, "config*.json") 42 | if err != nil { 43 | return err 44 | } 45 | if _, err := tmpfile.Write(file); err != nil { 46 | return err 47 | } 48 | if err := tmpfile.Close(); err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | 54 | func TestBackendConfigFileReader(t *testing.T) { 55 | tmpDir, err := ioutil.TempDir("", "file_config") 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | defer os.RemoveAll(tmpDir) 60 | createDir := func() string { 61 | path, err := ioutil.TempDir(tmpDir, "config") 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | return path 66 | } 67 | 68 | t.Run("should read no config files", func(t *testing.T) { 69 | tt := []struct { 70 | name string 71 | setup func(path string) 72 | }{ 73 | { 74 | name: "empty dir", 75 | setup: func(path string) {}, 76 | }, 77 | { 78 | name: "non existing dir", 79 | setup: func(path string) { 80 | os.Remove(path) 81 | }, 82 | }, 83 | { 84 | name: "only main config", 85 | setup: func(path string) { 86 | writeDefaultMainConfig(path) 87 | }, 88 | }, 89 | { 90 | name: "config without .json", 91 | setup: func(path string) { 92 | filename := filepath.Join(path, "serverconfig") 93 | os.WriteFile(filename, []byte{1, 2, 3, 4}, os.ModePerm) 94 | }, 95 | }, 96 | { 97 | name: "dir contains empty dir", 98 | setup: func(path string) { 99 | _, err := ioutil.TempDir(path, "empty") 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | }, 104 | }, 105 | } 106 | 107 | for _, tc := range tt { 108 | t.Run(tc.name, func(t *testing.T) { 109 | testDir := createDir() 110 | tc.setup(testDir) 111 | cfgReader := config.NewBackendConfigFileReader(testDir, emptyVerifyFunc) 112 | _, err := cfgReader.Read() 113 | if !errors.Is(err, config.ErrNoConfigFiles) { 114 | t.Errorf("expected no config files error but got: %v", err) 115 | } 116 | }) 117 | } 118 | }) 119 | 120 | t.Run("reads right amount of configs", func(t *testing.T) { 121 | testAmount := func(path string, expectedAmount int) { 122 | cfgReader := config.NewBackendConfigFileReader(path, emptyVerifyFunc) 123 | cfgs, err := cfgReader.Read() 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | 128 | if len(cfgs) != expectedAmount { 129 | t.Errorf("expected %d configs, but got %d", expectedAmount, len(cfgs)) 130 | } 131 | } 132 | 133 | t.Run("one config in main dir", func(t *testing.T) { 134 | testDir := createDir() 135 | expectedAmountConfigs := 1 136 | serverCfg := config.ServerConfig{ 137 | Domains: []string{"uv"}, 138 | ProxyTo: ":25566", 139 | } 140 | err := writeServerConfig(testDir, serverCfg) 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | testAmount(testDir, expectedAmountConfigs) 145 | }) 146 | 147 | t.Run("two configs in main dir", func(t *testing.T) { 148 | testDir := createDir() 149 | expectedAmountConfigs := 2 150 | serverCfg := config.ServerConfig{ 151 | Domains: []string{"uv"}, 152 | ProxyTo: ":25566", 153 | } 154 | serverCfg2 := config.ServerConfig{ 155 | Domains: []string{"uv1"}, 156 | ProxyTo: ":25567", 157 | } 158 | err := writeServerConfig(testDir, serverCfg) 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | err = writeServerConfig(testDir, serverCfg2) 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | testAmount(testDir, expectedAmountConfigs) 167 | }) 168 | 169 | t.Run("one config in sub dir", func(t *testing.T) { 170 | testDir := createDir() 171 | subDir, err := ioutil.TempDir(testDir, "subdir") 172 | if err != nil { 173 | t.Fatal(err) 174 | } 175 | expectedAmountConfigs := 1 176 | serverCfg := config.ServerConfig{ 177 | Domains: []string{"uv"}, 178 | ProxyTo: ":25566", 179 | } 180 | 181 | err = writeServerConfig(subDir, serverCfg) 182 | if err != nil { 183 | t.Fatal(err) 184 | } 185 | testAmount(testDir, expectedAmountConfigs) 186 | }) 187 | }) 188 | 189 | t.Run("verify func", func(t *testing.T) { 190 | testDir := createDir() 191 | serverCfg := config.ServerConfig{ 192 | Domains: []string{"uv"}, 193 | ProxyTo: ":25566", 194 | } 195 | err := writeServerConfig(testDir, serverCfg) 196 | if err != nil { 197 | t.Fatal(err) 198 | } 199 | t.Run("Calls verify func when there are configs to verify", func(t *testing.T) { 200 | counter := 0 201 | verifyFunc := func(cfgs []config.ServerConfig) error { 202 | counter++ 203 | return nil 204 | } 205 | cfgReader := config.NewBackendConfigFileReader(testDir, verifyFunc) 206 | _, err = cfgReader.Read() 207 | if counter != 1 { 208 | t.Errorf("expected to be called once but was called %d times", counter) 209 | } 210 | if err != nil { 211 | t.Error("didnt expect an error") 212 | } 213 | }) 214 | t.Run("returns error when there is an error", func(t *testing.T) { 215 | tErr := errors.New("test error") 216 | verifyFunc := func(cfgs []config.ServerConfig) error { 217 | return tErr 218 | } 219 | cfgReader := config.NewBackendConfigFileReader(testDir, verifyFunc) 220 | _, err = cfgReader.Read() 221 | if err == nil { 222 | t.Error("expected an error") 223 | } 224 | }) 225 | }) 226 | 227 | t.Run("should know their file path", func(t *testing.T) { 228 | testDir := createDir() 229 | verifyFunc := func(cfgs []config.ServerConfig) error { 230 | return nil 231 | } 232 | serverCfg := config.ServerConfig{ 233 | Domains: []string{"uv"}, 234 | ProxyTo: ":25566", 235 | } 236 | 237 | err = writeServerConfig(testDir, serverCfg) 238 | if err != nil { 239 | t.Fatal(err) 240 | } 241 | cfgReader := config.NewBackendConfigFileReader(testDir, verifyFunc) 242 | cfgs, err := cfgReader.Read() 243 | if err != nil { 244 | t.Fatal(err) 245 | } 246 | for _, cfg := range cfgs { 247 | if !strings.HasPrefix(cfg.FilePath, testDir) { 248 | t.Error("config should have know its config path") 249 | t.Log(testDir) 250 | t.Log(cfg.FilePath) 251 | } 252 | } 253 | }) 254 | 255 | } 256 | 257 | func TestReadServerConfigs(t *testing.T) { 258 | generateNumberOfFile := 3 259 | cfg := config.ServerConfig{ 260 | Domains: []string{"ultraviolet"}, 261 | ProxyTo: ":25566", 262 | } 263 | tt := []struct { 264 | testName string 265 | hasDifferentFile bool 266 | specialName string 267 | expectedReadFile int 268 | }{ 269 | { 270 | testName: "normal configs", 271 | hasDifferentFile: false, 272 | expectedReadFile: generateNumberOfFile, 273 | }, 274 | { 275 | testName: "doesnt read file with no extension", 276 | hasDifferentFile: true, 277 | specialName: "example*", 278 | expectedReadFile: generateNumberOfFile - 1, 279 | }, 280 | { 281 | testName: "doesnt read file with different extension", 282 | hasDifferentFile: true, 283 | specialName: "example*.yml", 284 | expectedReadFile: generateNumberOfFile - 1, 285 | }, 286 | { 287 | testName: "doesnt read ultraviolet config file", 288 | hasDifferentFile: true, 289 | specialName: config.MainConfigFileName, 290 | expectedReadFile: generateNumberOfFile - 1, 291 | }, 292 | } 293 | 294 | for _, tc := range tt { 295 | t.Run(tc.testName, func(t *testing.T) { 296 | tmpDir, _ := ioutil.TempDir("", "configs") 297 | bb, _ := json.MarshalIndent(cfg, "", " ") 298 | for i := 0; i < generateNumberOfFile; i++ { 299 | fileName := "example*.json" 300 | if tc.hasDifferentFile && i == 0 { 301 | fileName = tc.specialName 302 | } 303 | tmpfile, err := ioutil.TempFile(tmpDir, fileName) 304 | if err != nil { 305 | t.Fatal(err) 306 | } 307 | if _, err := tmpfile.Write(bb); err != nil { 308 | t.Fatal(err) 309 | } 310 | if err := tmpfile.Close(); err != nil { 311 | t.Fatal(err) 312 | } 313 | } 314 | loadedCfgs, _ := config.ReadServerConfigs(tmpDir) 315 | for i, loadedCfg := range loadedCfgs { 316 | loadedCfg.FilePath = "" 317 | if !reflect.DeepEqual(cfg, loadedCfg) { 318 | t.Errorf("index: %d \nWanted:%v \n got: %v", i, cfg, loadedCfg) 319 | } 320 | } 321 | if len(loadedCfgs) != tc.expectedReadFile { 322 | t.Errorf("Expected %v configs to be read but there are %d configs read", tc.expectedReadFile, len(loadedCfgs)) 323 | } 324 | }) 325 | } 326 | } 327 | 328 | func TestReadServerConfig(t *testing.T) { 329 | cfg := config.ServerConfig{ 330 | Domains: []string{"ultraviolet"}, 331 | ProxyTo: ":25566", 332 | } 333 | tmpfile, err := ioutil.TempFile("", "example*.json") 334 | cfg.FilePath = tmpfile.Name() 335 | file, _ := json.MarshalIndent(cfg, "", " ") 336 | if err != nil { 337 | t.Fatal(err) 338 | } 339 | defer os.Remove(tmpfile.Name()) 340 | if _, err := tmpfile.Write(file); err != nil { 341 | t.Fatal(err) 342 | } 343 | if err := tmpfile.Close(); err != nil { 344 | t.Fatal(err) 345 | } 346 | loadedCfg, err := config.LoadServerCfgFromPath(tmpfile.Name()) 347 | if err != nil { 348 | t.Error(err) 349 | } 350 | 351 | if !reflect.DeepEqual(cfg, loadedCfg) { 352 | t.Errorf("Wanted:%v \n got: %v", cfg, loadedCfg) 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /config/file_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/x509" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "os" 14 | "path/filepath" 15 | "reflect" 16 | "testing" 17 | "time" 18 | 19 | "github.com/realDragonium/Ultraviolet/config" 20 | "github.com/realDragonium/Ultraviolet/mc" 21 | ) 22 | 23 | func samePK(expected, received mc.Packet) bool { 24 | sameID := expected.ID == received.ID 25 | sameData := bytes.Equal(expected.Data, received.Data) 26 | return sameID && sameData 27 | } 28 | 29 | 30 | func TestReadUltravioletConfigFile(t *testing.T) { 31 | t.Run("normal config", func(t *testing.T) { 32 | cfg := config.UltravioletConfig{ 33 | ListenTo: ":25565", 34 | DefaultStatus: mc.SimpleStatus{ 35 | Name: "Ultraviolet", 36 | Protocol: 755, 37 | Description: "One dangerous proxy", 38 | }, 39 | NumberOfWorkers: 5, 40 | } 41 | 42 | file, _ := json.MarshalIndent(cfg, "", " ") 43 | tmpDir, err := ioutil.TempDir("", "uv-normal*") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | filename := filepath.Join(tmpDir, config.MainConfigFileName) 48 | os.WriteFile(filename, file, os.ModePerm) 49 | loadedCfg, err := config.ReadUltravioletConfig(tmpDir) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | expectedCfg, err := config.CombineUltravioletConfigs(config.DefaultUltravioletConfig(), cfg) 55 | if err != nil { 56 | t.Fatalf("didnt expect error but got: %v", err) 57 | } 58 | 59 | if !reflect.DeepEqual(expectedCfg, loadedCfg) { 60 | t.Fatalf("Wanted:%v \n got: %v", cfg, loadedCfg) 61 | } 62 | 63 | os.Remove(tmpDir) 64 | 65 | }) 66 | 67 | t.Run("creates folder if it does not exist", func(t *testing.T) { 68 | tmpDir, err := ioutil.TempDir("", "uv-normal*") 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | dirPath := filepath.Join(tmpDir, "uv-dir") 73 | config.ReadUltravioletConfig(dirPath) 74 | if _, err := os.Stat(dirPath); os.IsNotExist(err) { 75 | t.Errorf("there was no dir created at %s", dirPath) 76 | } 77 | os.Remove(tmpDir) 78 | }) 79 | 80 | t.Run("creates file if it does not exist", func(t *testing.T) { 81 | tmpDir, err := ioutil.TempDir("", "uv-normal*") 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | dirPath := filepath.Join(tmpDir, "uv-dir") 86 | cfg, err := config.ReadUltravioletConfig(dirPath) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | if _, err := os.Stat(dirPath); os.IsNotExist(err) { 91 | t.Errorf("there was no dir created at %s", dirPath) 92 | } 93 | 94 | expectedCfg := config.DefaultUltravioletConfig() 95 | if !reflect.DeepEqual(cfg, expectedCfg) { 96 | t.Log("expected config to be the same") 97 | t.Logf("expected: %v", expectedCfg) 98 | t.Fatalf("got: %v", cfg) 99 | } 100 | os.Remove(tmpDir) 101 | }) 102 | 103 | } 104 | 105 | func TestReadRealIPPrivateKeyFile(t *testing.T) { 106 | privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 107 | if err != nil { 108 | t.Fatalf("got error: %v", err) 109 | } 110 | keyBytes, err := x509.MarshalECPrivateKey(privKey) 111 | if err != nil { 112 | t.Fatalf("error during marshal key: %v", err) 113 | } 114 | 115 | tmpfile, err := ioutil.TempFile("", "example") 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | defer os.Remove(tmpfile.Name()) 120 | if _, err := tmpfile.Write(keyBytes); err != nil { 121 | t.Fatal(err) 122 | } 123 | if err := tmpfile.Close(); err != nil { 124 | t.Fatal(err) 125 | } 126 | readKey, err := config.ReadPrivateKey(tmpfile.Name()) 127 | if err != nil { 128 | t.Fatalf("error during key reading: %v", err) 129 | } 130 | 131 | if !readKey.Equal(privKey) { 132 | t.Logf("generatedKey: %v", privKey) 133 | t.Logf("readKey: %v", readKey) 134 | t.Fatal("Keys arent the same!") 135 | } 136 | } 137 | 138 | func TestReadRealIPPrivateKey_NonExistingFile_ReturnsError(t *testing.T) { 139 | fileName := "this-private-key" 140 | tmpDir, _ := ioutil.TempDir("", "configs") 141 | filePath := filepath.Join(tmpDir, fileName) 142 | _, err := config.ReadPrivateKey(filePath) 143 | if !errors.Is(err, os.ErrNotExist) { 144 | t.Fatalf("error during key reading: %v", err) 145 | } 146 | os.Remove(tmpDir) 147 | } 148 | 149 | func TestFileToWorkerConfig(t *testing.T) { 150 | t.Run("filled in values should match", func(t *testing.T) { 151 | serverCfg := config.ServerConfig{ 152 | Name: "UV", 153 | Domains: []string{"Ultraviolet", "Ultraviolet2", "UltraV", "UV"}, 154 | ProxyTo: "127.0.10.5:25565", 155 | ProxyBind: "127.0.0.5", 156 | OldRealIP: true, 157 | DialTimeout: "1s", 158 | SendProxyProtocol: true, 159 | DisconnectMessage: "HelloThereWeAreClosed...Sorry", 160 | OfflineStatus: mc.SimpleStatus{ 161 | Name: "Ultraviolet", 162 | Protocol: 755, 163 | Description: "Some broken proxy", 164 | }, 165 | RateLimit: 5, 166 | RateDuration: "1m", 167 | StateUpdateCooldown: "1m", 168 | } 169 | 170 | expectedDisconPk := mc.ClientBoundDisconnect{ 171 | Reason: mc.String(serverCfg.DisconnectMessage), 172 | }.Marshal() 173 | expectedOfflineStatus := mc.SimpleStatus{ 174 | Name: "Ultraviolet", 175 | Protocol: 755, 176 | Description: "Some broken proxy", 177 | }.Marshal() 178 | expectedRateDuration := 1 * time.Minute 179 | expectedUpdateCooldown := 1 * time.Minute 180 | expectedDialTimeout := 1 * time.Second 181 | 182 | workerCfg, err := config.ServerToBackendConfig(serverCfg) 183 | if err != nil { 184 | t.Fatalf("received unexpected error: %v", err) 185 | } 186 | 187 | if workerCfg.Name != serverCfg.Name { 188 | t.Errorf("expected: %v - got: %v", serverCfg.Name, workerCfg.Name) 189 | } 190 | if workerCfg.ProxyTo != serverCfg.ProxyTo { 191 | t.Errorf("expected: %v - got: %v", serverCfg.ProxyTo, workerCfg.ProxyTo) 192 | } 193 | if workerCfg.ProxyBind != serverCfg.ProxyBind { 194 | t.Errorf("expected: %v - got: %v", serverCfg.ProxyBind, workerCfg.ProxyBind) 195 | } 196 | if workerCfg.SendProxyProtocol != serverCfg.SendProxyProtocol { 197 | t.Errorf("expected: %v - got: %v", serverCfg.SendProxyProtocol, workerCfg.SendProxyProtocol) 198 | } 199 | if workerCfg.RateLimit != serverCfg.RateLimit { 200 | t.Errorf("expected: %v - got: %v", serverCfg.RateLimit, workerCfg.RateLimit) 201 | } 202 | if workerCfg.OldRealIp != serverCfg.OldRealIP { 203 | t.Errorf("expected: %v - got: %v", serverCfg.OldRealIP, workerCfg.OldRealIp) 204 | } 205 | if expectedRateDuration != workerCfg.RateLimitDuration { 206 | t.Errorf("expected: %v - got: %v", expectedRateDuration, workerCfg.RateLimitDuration) 207 | } 208 | if expectedUpdateCooldown != workerCfg.StateUpdateCooldown { 209 | t.Errorf("expected: %v - got: %v", expectedRateDuration, workerCfg.StateUpdateCooldown) 210 | } 211 | if expectedDialTimeout != workerCfg.DialTimeout { 212 | t.Errorf("expected: %v - got: %v", expectedDialTimeout, workerCfg.DialTimeout) 213 | } 214 | if !samePK(expectedOfflineStatus, workerCfg.OfflineStatus) { 215 | offlineStatus, _ := mc.UnmarshalClientBoundResponse(expectedOfflineStatus) 216 | receivedStatus, _ := mc.UnmarshalClientBoundResponse(workerCfg.OfflineStatus) 217 | t.Errorf("expcted: %v \ngot: %v", offlineStatus, receivedStatus) 218 | } 219 | 220 | if !samePK(expectedDisconPk, workerCfg.DisconnectPacket) { 221 | expectedDiscon, _ := mc.UnmarshalClientDisconnect(expectedDisconPk) 222 | receivedDiscon, _ := mc.UnmarshalClientDisconnect(workerCfg.DisconnectPacket) 223 | t.Errorf("expcted: %v \ngot: %v", expectedDiscon, receivedDiscon) 224 | } 225 | }) 226 | 227 | t.Run("when no name, first domain will be name", func(t *testing.T) { 228 | serverCfg := config.ServerConfig{ 229 | Domains: []string{"Ultraviolet", "UV"}, 230 | ProxyTo: "1", 231 | } 232 | workerCfg, err := config.ServerToBackendConfig(serverCfg) 233 | if err != nil { 234 | t.Fatalf("received unexpected error: %v", err) 235 | } 236 | if workerCfg.Name != serverCfg.Domains[0] { 237 | t.Errorf("expected: %v - got: %v", serverCfg.Domains[0], workerCfg.Name) 238 | } 239 | }) 240 | 241 | } 242 | 243 | func TestFileToWorkerConfig_NewRealIP_ReadsKeyCorrectly(t *testing.T) { 244 | privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 245 | if err != nil { 246 | t.Fatalf("error during creating privatekey: %v", err) 247 | } 248 | keyBytes, err := x509.MarshalECPrivateKey(privKey) 249 | if err != nil { 250 | t.Fatalf("error during marshal key: %v", err) 251 | } 252 | 253 | keyFile, err := ioutil.TempFile("", "example") 254 | if err != nil { 255 | t.Fatal(err) 256 | } 257 | defer os.Remove(keyFile.Name()) 258 | if _, err := keyFile.Write(keyBytes); err != nil { 259 | t.Fatal(err) 260 | } 261 | if err := keyFile.Close(); err != nil { 262 | t.Fatal(err) 263 | } 264 | keyPath := keyFile.Name() 265 | serverCfg := config.ServerConfig{ 266 | Domains: []string{"Ultraviolet"}, 267 | NewRealIP: true, 268 | RealIPKey: keyPath, 269 | ProxyTo: "1", 270 | } 271 | 272 | workerCfg, err := config.ServerToBackendConfig(serverCfg) 273 | if err != nil { 274 | t.Fatalf("received unexpected error: %v", err) 275 | } 276 | 277 | if !workerCfg.RealIPKey.Equal(privKey) { 278 | t.Logf("generatedKey: %v", privKey) 279 | t.Logf("readKey: %v", workerCfg.RealIPKey) 280 | t.Fatal("Keys arent the same!") 281 | } 282 | } 283 | 284 | func TestFileToWorkerConfig_NewRealIP_GenerateKeyCorrect(t *testing.T) { 285 | tmpDir, _ := ioutil.TempDir("", "configs") 286 | defer os.Remove(tmpDir) 287 | cfgPath := filepath.Join(tmpDir, "config") 288 | firstDomainName := "Ultraviolet" 289 | keyPrivatePath := filepath.Join(tmpDir, fmt.Sprintf("%s-%s", firstDomainName, "private.key")) 290 | keyPublicPath := filepath.Join(tmpDir, fmt.Sprintf("%s-%s", firstDomainName, "public.key")) 291 | serverCfg := config.ServerConfig{ 292 | FilePath: cfgPath, 293 | Domains: []string{firstDomainName}, 294 | NewRealIP: true, 295 | ProxyTo: "1", 296 | } 297 | 298 | workerCfg, err := config.ServerToBackendConfig(serverCfg) 299 | if err != nil { 300 | t.Fatalf("received unexpected error: %v", err) 301 | } 302 | 303 | readKey, err := config.ReadPrivateKey(keyPrivatePath) 304 | if err != nil { 305 | t.Fatalf("error during key reading: %v", err) 306 | } 307 | if !reflect.DeepEqual(workerCfg.RealIPKey, readKey) { 308 | t.Logf("generatedKey: %v", workerCfg.RealIPKey) 309 | t.Logf("readKey: %v", readKey) 310 | t.Fatal("Private keys arent the same!") 311 | } 312 | 313 | bb, err := ioutil.ReadFile(keyPublicPath) 314 | if err != nil { 315 | t.Fatalf("error during key reading: %v", err) 316 | } 317 | pub, err := x509.ParsePKIXPublicKey(bb) 318 | if err != nil { 319 | t.Fatalf("didnt expect error but got: %v", err) 320 | } 321 | pubkey := pub.(*ecdsa.PublicKey) 322 | newPubkey := workerCfg.RealIPKey.PublicKey 323 | if !reflect.DeepEqual(*pubkey, newPubkey) { 324 | t.Logf("generatedKey: %v", newPubkey) 325 | t.Logf("readKey: %v", pubkey) 326 | t.Fatal("Public keys arent the same!") 327 | } 328 | 329 | } 330 | -------------------------------------------------------------------------------- /config/types.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "io" 6 | "os" 7 | "time" 8 | 9 | "github.com/realDragonium/Ultraviolet/mc" 10 | ) 11 | 12 | type ServerConfig struct { 13 | FilePath string 14 | Name string `json:"name"` 15 | Domains []string `json:"domains"` 16 | 17 | ProxyTo string `json:"proxyTo"` 18 | ProxyBind string `json:"proxyBind"` 19 | DialTimeout string `json:"dialTimeout"` 20 | OldRealIP bool `json:"useRealIPv2.4"` 21 | NewRealIP bool `json:"useRealIPv2.5"` 22 | RealIPKey string `json:"realIPKeyPath"` 23 | SendProxyProtocol bool `json:"sendProxyProtocol"` 24 | 25 | DisconnectMessage string `json:"disconnectMessage"` 26 | 27 | CacheStatus bool `json:"cacheStatus"` 28 | CacheUpdateCooldown string `json:"cacheUpdateCooldown"` 29 | ValidProtocol int `json:"validProtocol"` 30 | OfflineStatus mc.SimpleStatus `json:"offlineStatus"` 31 | 32 | RateLimit int `json:"rateLimit"` 33 | RateDuration string `json:"rateCooldown"` 34 | RateBanListCooldown string `json:"banListCooldown"` 35 | RateDisconMsg string `json:"reconnectMsg"` 36 | 37 | CheckStateOption string 38 | StateUpdateCooldown string `json:"stateUpdateCooldown"` 39 | } 40 | 41 | type APIServerConfig struct { 42 | ID string `json:"id"` 43 | 44 | Domains []string `json:"domains"` 45 | ProxyTo string `json:"proxyTo"` 46 | ProxyBind string `json:"proxyBind"` 47 | DialTimeout string `json:"dialTimeout"` 48 | SendProxyProtocol bool `json:"sendProxyProtocol"` 49 | 50 | IsOnline bool `json:"isOnline"` 51 | UseStatusCache bool `json:"useStatusCache"` 52 | CachedStatus mc.SimpleStatus `json:"cachedStatus"` 53 | DisconnectMessage string `json:"disconnectMessage"` 54 | 55 | LimitBots bool 56 | } 57 | 58 | func (cfg ServerConfig) ID() string { 59 | return cfg.FilePath 60 | } 61 | 62 | func DefaultServerConfig() ServerConfig { 63 | return ServerConfig{ 64 | ProxyBind: "", 65 | DialTimeout: "1s", 66 | OldRealIP: false, 67 | NewRealIP: false, 68 | SendProxyProtocol: false, 69 | DisconnectMessage: "{\"text\": \"Server is offline\"}", 70 | CacheStatus: true, 71 | CacheUpdateCooldown: "1m", 72 | RateLimit: 5, 73 | RateDuration: "1s", 74 | RateBanListCooldown: "5m", 75 | RateDisconMsg: "{\"text\": \"Please reconnect to verify yourself\"}", 76 | StateUpdateCooldown: "5s", 77 | } 78 | } 79 | 80 | type UltravioletConfig struct { 81 | ListenTo string `json:"listenTo"` 82 | DefaultStatus mc.SimpleStatus `json:"defaultStatus"` 83 | VerifyConnMsg string `json:"verifyConnMsg"` 84 | NumberOfWorkers int `json:"numberOfWorkers"` 85 | NumberOfListeners int `json:"numberOfListeners"` 86 | AcceptProxyProtocol bool `json:"acceptProxyProtocol"` 87 | UsePrometheus bool `json:"enablePrometheus"` 88 | PrometheusBind string `json:"prometheusBind"` 89 | APIBind string `json:"apiBind"` 90 | UseTableflip bool `json:"useTableflip"` 91 | PidFile string `json:"pidFile"` 92 | UseLessStableMode bool `json:"useLessStableMode"` 93 | 94 | IODeadline time.Duration 95 | LogOutput io.Writer 96 | } 97 | 98 | func (cfg UltravioletConfig) VerifyConnectionPk() mc.Packet { 99 | return mc.ClientBoundDisconnect{ 100 | Reason: mc.String(cfg.VerifyConnMsg), 101 | }.Marshal() 102 | } 103 | 104 | func (cfg UltravioletConfig) DefaultStatusPk() mc.Packet { 105 | return cfg.DefaultStatus.Marshal() 106 | } 107 | 108 | func DefaultUltravioletConfig() UltravioletConfig { 109 | return UltravioletConfig{ 110 | ListenTo: ":25565", 111 | DefaultStatus: mc.SimpleStatus{ 112 | Name: "Ultraviolet", 113 | Protocol: 0, 114 | Description: "Some proxy didnt proxy", 115 | }, 116 | VerifyConnMsg: "{\"text\": \"Please reconnect to verify yourself\"}", 117 | NumberOfWorkers: 10, 118 | NumberOfListeners: 1, 119 | AcceptProxyProtocol: false, 120 | UsePrometheus: true, 121 | PrometheusBind: ":9100", 122 | APIBind: "127.0.0.1:9099", 123 | PidFile: "", 124 | UseTableflip: true, 125 | UseLessStableMode: false, 126 | 127 | IODeadline: time.Second, 128 | LogOutput: os.Stdout, 129 | } 130 | } 131 | 132 | type StateOptions int 133 | 134 | const ( 135 | _ StateOptions = iota 136 | CACHE 137 | ALWAYS_ONLINE 138 | ALWAYS_OFFLINE 139 | ) 140 | 141 | func NewStateOption(option string) StateOptions { 142 | o := CACHE 143 | switch option { 144 | case "online": 145 | o = ALWAYS_ONLINE 146 | case "offline": 147 | o = ALWAYS_OFFLINE 148 | } 149 | return o 150 | } 151 | 152 | type BackendWorkerConfig struct { 153 | Name string 154 | StateOption StateOptions 155 | StateUpdateCooldown time.Duration 156 | OldRealIp bool 157 | NewRealIP bool 158 | RealIPKey *ecdsa.PrivateKey 159 | CacheStatus bool 160 | CacheUpdateCooldown time.Duration 161 | ValidProtocol int 162 | OfflineStatus mc.Packet 163 | DisconnectPacket mc.Packet 164 | ProxyTo string 165 | ProxyBind string 166 | DialTimeout time.Duration 167 | SendProxyProtocol bool 168 | RateLimit int 169 | RateLimitStatus bool 170 | RateLimitDuration time.Duration 171 | RateBanListCooldown time.Duration 172 | RateDisconPk mc.Packet 173 | } 174 | 175 | func DefaultWorkerConfig() WorkerConfig { 176 | return WorkerConfig{ 177 | IOTimeout: time.Second, 178 | } 179 | } 180 | 181 | type WorkerConfig struct { 182 | NumberOfWorkers int 183 | DefaultStatus mc.SimpleStatus 184 | IOTimeout time.Duration 185 | } 186 | 187 | func NewWorkerConfig(uvCfg UltravioletConfig) WorkerConfig { 188 | if uvCfg.IODeadline == 0 { 189 | uvCfg.IODeadline = time.Second 190 | } 191 | return WorkerConfig{ 192 | DefaultStatus: uvCfg.DefaultStatus, 193 | IOTimeout: uvCfg.IODeadline, 194 | } 195 | } 196 | 197 | type UVConfigReader = func() (UltravioletConfig, error) 198 | 199 | // Will only return the configs if they are deemed usable 200 | // If they contain conflicts of something goes wrong while reading 201 | // it will return a error 202 | type ServerConfigReader = func() ([]ServerConfig, error) 203 | -------------------------------------------------------------------------------- /config/verify.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func NewVerifyError() verifyError { 9 | return verifyError{ 10 | errors: []error{}, 11 | } 12 | } 13 | 14 | type verifyError struct { 15 | errors []error 16 | } 17 | 18 | func (vErr *verifyError) Error() string { 19 | var sb strings.Builder 20 | sb.WriteString("The following errors have been found: ") 21 | for _, err := range vErr.errors { 22 | sb.WriteString("\n") 23 | sb.WriteString(err.Error()) 24 | } 25 | return sb.String() 26 | } 27 | 28 | func (err *verifyError) HasErrors() bool { 29 | return len(err.errors) > 0 30 | } 31 | 32 | func (vErr *verifyError) Add(err error) { 33 | vErr.errors = append(vErr.errors, err) 34 | } 35 | 36 | type DuplicateDomain struct { 37 | Cfg1Path string 38 | Cfg2Path string 39 | Domain string 40 | } 41 | 42 | func (err *DuplicateDomain) Error() string { 43 | return fmt.Sprintf("'%s' has been found in %s and %s", err.Domain, err.Cfg1Path, err.Cfg2Path) 44 | } 45 | 46 | type VerifyFunc func(cfgs []ServerConfig) error 47 | 48 | func VerifyConfigs(cfgs []ServerConfig) error { 49 | vErrors := NewVerifyError() 50 | domains := make(map[string]int) 51 | for index, cfg := range cfgs { 52 | if len(cfg.Domains) == 0 { 53 | err := fmt.Errorf("'domains' is not allowed to be empty in %s", cfg.FilePath) 54 | vErrors.Add(err) 55 | } 56 | if cfg.ProxyTo == "" { 57 | err := fmt.Errorf("'proxyTo' is not allowed to be empty in %s", cfg.FilePath) 58 | vErrors.Add(err) 59 | } 60 | for _, domain := range cfg.Domains { 61 | otherIndex, ok := domains[domain] 62 | if ok { 63 | err := &DuplicateDomain{ 64 | Domain: domain, 65 | Cfg1Path: cfg.FilePath, 66 | Cfg2Path: cfgs[otherIndex].FilePath, 67 | } 68 | vErrors.Add(err) 69 | continue 70 | } 71 | domains[domain] = index 72 | } 73 | } 74 | if vErrors.HasErrors() { 75 | return &vErrors 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /config/verify_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/realDragonium/Ultraviolet/config" 7 | ) 8 | 9 | func TestVerifyConfigs(t *testing.T) { 10 | t.Run("can detect duplicate domains", func(t *testing.T) { 11 | domain := "uv" 12 | cfgs := []config.ServerConfig{ 13 | { 14 | FilePath: "uv1", 15 | Domains: []string{domain}, 16 | ProxyTo: "1", 17 | }, 18 | { 19 | FilePath: "uv2", 20 | Domains: []string{domain}, 21 | ProxyTo: "1", 22 | }, 23 | } 24 | 25 | err := config.VerifyConfigs(cfgs) 26 | if err == nil { 27 | t.Fatal("expected it to have errors") 28 | } 29 | // _, ok := errs[0].(*config.DuplicateDomain) 30 | // if !ok { 31 | // t.Errorf("expected DuplicateDomain but got %T", errs[0]) 32 | // } 33 | }) 34 | 35 | t.Run("can detect multiple duplicate domains", func(t *testing.T) { 36 | domain := "uv" 37 | cfgs := []config.ServerConfig{ 38 | { 39 | FilePath: "uv1", 40 | Domains: []string{domain}, 41 | ProxyTo: "1", 42 | }, 43 | { 44 | FilePath: "uv2", 45 | Domains: []string{domain}, 46 | ProxyTo: "1", 47 | }, 48 | { 49 | FilePath: "uv3", 50 | Domains: []string{domain}, 51 | ProxyTo: "1", 52 | }, 53 | } 54 | 55 | err := config.VerifyConfigs(cfgs) 56 | if err == nil { 57 | t.Fatal("expected it to have errors") 58 | // t.Fatalf("expected 2 errors but got %d", len(errs)) 59 | } 60 | }) 61 | 62 | t.Run("returns error when there are no domains", func(t *testing.T) { 63 | cfgs := []config.ServerConfig{ 64 | { 65 | ProxyTo: ":9284", 66 | }, 67 | } 68 | err := config.VerifyConfigs(cfgs) 69 | if err == nil { 70 | t.Log("expect it to have an error") 71 | // t.Errorf("expected no domain error but instead got: %v", err) 72 | } 73 | }) 74 | 75 | t.Run("returns error when there is no target", func(t *testing.T) { 76 | cfgs := []config.ServerConfig{ 77 | { 78 | Domains: []string{"uv"}, 79 | }, 80 | } 81 | err := config.VerifyConfigs(cfgs) 82 | if err == nil { 83 | t.Log("expect it to have an error") 84 | // t.Errorf("expected no domain error but instead got: %v", err) 85 | } 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /core/errors.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrClientToSlow = errors.New("client was to slow with sending its packets") 7 | ErrClientClosedConn = errors.New("client closed the connection") 8 | ErrNoServerFound = errors.New("could not find server") 9 | ErrNoServerConn = errors.New("could not find server") 10 | ErrNotValidHandshake = errors.New("not a valid handshake state") 11 | ErrOverConnRateLimit = errors.New("too many request within rate limit time frame") 12 | ErrStatusPing = errors.New("something went wrong while pinging") 13 | ) 14 | -------------------------------------------------------------------------------- /core/proxy.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/realDragonium/Ultraviolet/config" 7 | ) 8 | 9 | type NewProxyFunc func(config.UVConfigReader, net.Listener, config.ServerConfigReader) Proxy 10 | 11 | type Proxy interface { 12 | Start() error 13 | } 14 | -------------------------------------------------------------------------------- /core/request_data.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/realDragonium/Ultraviolet/mc" 7 | ) 8 | 9 | type RequestData struct { 10 | Type mc.HandshakeState 11 | Handshake mc.ServerBoundHandshake 12 | ServerAddr string 13 | Addr net.Addr 14 | Username string 15 | } 16 | -------------------------------------------------------------------------------- /core/server.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/realDragonium/Ultraviolet/mc" 7 | ) 8 | 9 | type Server interface { 10 | ConnAction(req RequestData) ServerAction 11 | CreateConn(req RequestData) (c net.Conn, err error) 12 | Status() mc.Packet 13 | } 14 | 15 | type ServerAction byte 16 | 17 | const ( 18 | // IDEA: Some errors could replace these 'actions' 19 | CLOSE ServerAction = iota 20 | DEFAULT_STATUS 21 | STATUS 22 | VERIFY_CONN 23 | DISCONNECT 24 | PROXY 25 | PROXY_REALIP_2_4 26 | PROXY_REALIP_2_5 27 | ) 28 | 29 | //go:generate stringer -type=ServerState 30 | type ServerState byte 31 | 32 | const ( 33 | Unknown ServerState = iota 34 | Online 35 | Offline 36 | ) 37 | -------------------------------------------------------------------------------- /core/server_catalog.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/realDragonium/Ultraviolet/mc" 7 | ) 8 | 9 | type ServerCatalog interface { 10 | Find(addr string) (Server, error) 11 | DefaultStatus() mc.Packet 12 | VerifyConn() mc.Packet 13 | } 14 | 15 | func NewServerCatalog(servers map[string]Server, defaultStatusPk, verifyConnPk mc.Packet) ServerCatalog { 16 | return BasicServerCatalog{ 17 | ServerDict: servers, 18 | defaultStatusPk: defaultStatusPk, 19 | verifyConnPk: verifyConnPk, 20 | } 21 | } 22 | 23 | func NewEmptyServerCatalog(defaultStatusPk, verifyConnPk mc.Packet) BasicServerCatalog { 24 | return BasicServerCatalog{ 25 | ServerDict: make(map[string]Server), 26 | defaultStatusPk: defaultStatusPk, 27 | verifyConnPk: verifyConnPk, 28 | } 29 | } 30 | 31 | type BasicServerCatalog struct { 32 | ServerDict map[string]Server 33 | defaultStatusPk mc.Packet 34 | verifyConnPk mc.Packet 35 | } 36 | 37 | func (catalog BasicServerCatalog) Find(addr string) (Server, error) { 38 | cleanAddr := strings.ToLower(addr) 39 | server, ok := catalog.ServerDict[cleanAddr] 40 | if !ok { 41 | return nil, ErrNoServerFound 42 | } 43 | return server, nil 44 | } 45 | 46 | func (catalog BasicServerCatalog) DefaultStatus() mc.Packet { 47 | return catalog.defaultStatusPk 48 | } 49 | 50 | func (catalog BasicServerCatalog) VerifyConn() mc.Packet { 51 | return catalog.verifyConnPk 52 | } 53 | -------------------------------------------------------------------------------- /core/serverstate_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ServerState"; DO NOT EDIT. 2 | 3 | package core 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[Unknown-0] 12 | _ = x[Online-1] 13 | _ = x[Offline-2] 14 | } 15 | 16 | const _ServerState_name = "UnknownOnlineOffline" 17 | 18 | var _ServerState_index = [...]uint8{0, 7, 13, 20} 19 | 20 | func (i ServerState) String() string { 21 | if i >= ServerState(len(_ServerState_index)-1) { 22 | return "ServerState(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _ServerState_name[_ServerState_index[i]:_ServerState_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /examples/minimal.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "domains": ["localhost"], 3 | "proxyTo": ":25566" 4 | } 5 | -------------------------------------------------------------------------------- /examples/server-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "domains": ["localhost", "127.0.0.1", "0.0.0.0"], 3 | "proxyTo": ":25566", 4 | "proxyBind": "127.0.0.1", 5 | "dialTimeout": "1s", 6 | "sendProxyProtocol": false, 7 | "disconnectMessage": "{\"text\":\"Sorry, but the server is offline.\"}", 8 | "cacheStatus": true, 9 | "validProtocol": 755, 10 | "cacheUpdateCooldown": "5s", 11 | "offlineStatus": { 12 | "name": "Ultraviolet", 13 | "protocol": 755, 14 | "text": "Some offline server" 15 | }, 16 | "rateLimit": 10, 17 | "rateCooldown": "4s", 18 | "stateUpdateCooldown": "10s" 19 | } 20 | -------------------------------------------------------------------------------- /examples/ultraviolet.json: -------------------------------------------------------------------------------- 1 | { 2 | "listenTo": ":25565", 3 | "defaultStatus": { 4 | "name": "Ultraviolet", 5 | "protocol": 755, 6 | "text": "Proxy message" 7 | }, 8 | "numberOfWorkers": 10, 9 | "numberOfListeners": 1, 10 | "acceptProxyProtocol": false, 11 | "useTableflip": true, 12 | "pidFile": "/etc/ultraviolet/uv.pid" 13 | } -------------------------------------------------------------------------------- /examples/ultraviolet.service.example: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Ultraviolet - A Minecraft reverse proxy 3 | 4 | [Service] 5 | ExecStart=/usr/bin/ultraviolet run 6 | ExecReload=/bin/kill -HUP $MAINPID 7 | PIDFile=/etc/ultraviolet/uv.pid 8 | 9 | [Install] 10 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/realDragonium/Ultraviolet 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/cloudflare/tableflip v1.2.2 7 | github.com/google/go-cmp v0.5.7 8 | github.com/pires/go-proxyproto v0.6.1 9 | github.com/prometheus/client_golang v1.12.1 10 | ) 11 | -------------------------------------------------------------------------------- /mc/conn.go: -------------------------------------------------------------------------------- 1 | package mc 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | 7 | 8 | ) 9 | 10 | 11 | type McConn interface{ 12 | ReadPacket() (Packet, error) 13 | WritePacket(p Packet) error 14 | } 15 | 16 | func NewMcConn(conn net.Conn) mcConn { 17 | return mcConn{ 18 | netConn: conn, 19 | reader: bufio.NewReader(conn), 20 | } 21 | } 22 | 23 | type mcConn struct { 24 | netConn net.Conn 25 | reader DecodeReader 26 | } 27 | 28 | func (conn mcConn) ReadPacket() (Packet, error) { 29 | pk, err := ReadPacket(conn.reader) 30 | return pk, err 31 | } 32 | 33 | func (conn mcConn) WritePacket(p Packet) error { 34 | bytes := p.Marshal() 35 | _, err := conn.netConn.Write(bytes) 36 | return err 37 | } 38 | 39 | func (conn mcConn) WriteMcPacket(s McPacket) error { 40 | p := s.MarshalPacket() 41 | bytes := p.Marshal() 42 | _, err := conn.netConn.Write(bytes) 43 | return err 44 | } 45 | -------------------------------------------------------------------------------- /mc/handshakestate_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=HandshakeState"; DO NOT EDIT. 2 | 3 | package mc 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[UnknownState-0] 12 | _ = x[Status-1] 13 | _ = x[Login-2] 14 | } 15 | 16 | const _HandshakeState_name = "UnknownStateStatusLogin" 17 | 18 | var _HandshakeState_index = [...]uint8{0, 12, 18, 23} 19 | 20 | func (i HandshakeState) String() string { 21 | if i >= HandshakeState(len(_HandshakeState_index)-1) { 22 | return "HandshakeState(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _HandshakeState_name[_HandshakeState_index[i]:_HandshakeState_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /mc/packet.go: -------------------------------------------------------------------------------- 1 | package mc 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | ) 10 | 11 | var ( 12 | ErrInvalidPacketID = errors.New("invalid packet id") 13 | ErrPacketTooBig = errors.New("packet contains too much data") 14 | MaxPacketSize = 2097151 15 | ) 16 | 17 | const ( 18 | ServerBoundHandshakePacketID byte = 0x00 19 | HandshakePacketID int = 0x00 20 | 21 | StatusState = 1 22 | LoginState = 2 23 | 24 | HandshakeStatusState = VarInt(StatusState) 25 | HandshakeLoginState = VarInt(LoginState) 26 | 27 | ForgeSeparator = "\x00" 28 | RealIPSeparator = "///" 29 | ) 30 | 31 | // Packet is the raw representation of message that is send between the client and the server 32 | type Packet struct { 33 | ID byte 34 | Data []byte 35 | } 36 | 37 | type McPacket interface { 38 | MarshalPacket() Packet 39 | } 40 | 41 | // Scan decodes and copies the Packet data into the fields 42 | func (pk Packet) Scan(fields ...FieldDecoder) error { 43 | return ScanFields(bytes.NewReader(pk.Data), fields...) 44 | } 45 | 46 | // Marshal encodes the packet and all it's fields 47 | func (pk *Packet) Marshal() []byte { 48 | var packedData []byte 49 | data := []byte{pk.ID} 50 | data = append(data, pk.Data...) 51 | packetLength := VarInt(int32(len(data))).Encode() 52 | packedData = append(packedData, packetLength...) 53 | 54 | return append(packedData, data...) 55 | } 56 | 57 | // ScanFields decodes a byte stream into fields 58 | func ScanFields(r DecodeReader, fields ...FieldDecoder) error { 59 | for _, field := range fields { 60 | if err := field.Decode(r); err != nil { 61 | return err 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | // MarshalPacket transforms an ID and Fields into a Packet 68 | func MarshalPacket(ID byte, fields ...FieldEncoder) Packet { 69 | var pkt Packet 70 | pkt.ID = ID 71 | 72 | for _, v := range fields { 73 | pkt.Data = append(pkt.Data, v.Encode()...) 74 | } 75 | 76 | return pkt 77 | } 78 | 79 | // ReadPacketBytes decodes a byte stream and cuts the first Packet as a byte array out 80 | func ReadPacketBytes(r DecodeReader) ([]byte, error) { 81 | var packetLength VarInt 82 | if err := packetLength.Decode(r); err != nil { 83 | return nil, err 84 | } 85 | 86 | if packetLength < 1 { 87 | return nil, fmt.Errorf("packet length too short") 88 | } 89 | 90 | data := make([]byte, packetLength) 91 | if _, err := io.ReadFull(r, data); err != nil { 92 | return nil, fmt.Errorf("reading the content of the packet failed: %v", err) 93 | } 94 | 95 | return data, nil 96 | } 97 | 98 | // ReadPacketOld decodes and decompresses a byte stream and cuts the first Packet out 99 | func ReadPacketOld(r DecodeReader) (Packet, error) { 100 | data, err := ReadPacketBytes(r) 101 | 102 | if err != nil { 103 | return Packet{}, err 104 | } 105 | 106 | return Packet{ 107 | ID: data[0], 108 | Data: data[1:], 109 | }, nil 110 | } 111 | 112 | func ReadPacket(r DecodeReader) (Packet, error) { 113 | packetLength, err := ReadVarInt(r) 114 | if err != nil { 115 | return Packet{}, err 116 | } 117 | 118 | if packetLength < 1 { 119 | return Packet{}, fmt.Errorf("packet length too short") 120 | } 121 | 122 | data := make([]byte, packetLength) 123 | if _, err := io.ReadFull(r, data); err != nil { 124 | return Packet{}, fmt.Errorf("reading the content of the packet failed: %v", err) 125 | } 126 | 127 | return Packet{ 128 | ID: data[0], 129 | Data: data[1:], 130 | }, nil 131 | } 132 | 133 | func ReadPacket_WithBytes(b []byte) (Packet, error) { 134 | buf := bytes.NewBuffer(b) 135 | reader := bufio.NewReader(buf) 136 | return ReadPacket3(reader) 137 | } 138 | 139 | func ReadPacket3(r *bufio.Reader) (Packet, error) { 140 | packetLength, err := ReadVarInt_ByteReader(r) 141 | if err != nil { 142 | return Packet{}, err 143 | } 144 | 145 | if packetLength < 1 { 146 | return Packet{}, fmt.Errorf("packet length too short") 147 | } 148 | data := make([]byte, packetLength) 149 | if _, err := io.ReadFull(r, data); err != nil { 150 | return Packet{}, fmt.Errorf("reading the content of the packet failed: %v", err) 151 | } 152 | 153 | return Packet{ 154 | ID: data[0], 155 | Data: data[1:], 156 | }, nil 157 | } 158 | 159 | func ReadPacket3_Handshake(r *bufio.Reader) (ServerBoundHandshake, error) { 160 | var hs ServerBoundHandshake 161 | packetLength, err := ReadVarInt_ByteReader(r) 162 | if err != nil { 163 | return hs, err 164 | } 165 | 166 | if packetLength < 1 { 167 | return hs, fmt.Errorf("packet length too short") 168 | } 169 | 170 | packetID, err := ReadVarInt_ByteReader(r) 171 | if err != nil { 172 | return hs, err 173 | } 174 | if packetID != HandshakePacketID { 175 | return hs, ErrInvalidPacketID 176 | } 177 | 178 | hs.ProtocolVersion, err = ReadVarInt_ByteReader(r) 179 | if err != nil { 180 | return hs, err 181 | } 182 | hs.ServerAddress, err = ReadString_ByteReader(r) 183 | if err != nil { 184 | return hs, err 185 | } 186 | hs.ServerPort, err = ReadShot_ByteReader(r) 187 | if err != nil { 188 | return hs, err 189 | } 190 | state, err := ReadVarInt_ByteReader(r) 191 | if err != nil { 192 | return hs, err 193 | } 194 | hs.NextState = byte(state) 195 | 196 | return hs, nil 197 | } 198 | -------------------------------------------------------------------------------- /mc/packet_login.go: -------------------------------------------------------------------------------- 1 | package mc 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/rand" 7 | "crypto/sha512" 8 | "encoding/base64" 9 | "fmt" 10 | "io" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | //go:generate stringer -type=HandshakeState 16 | type HandshakeState byte 17 | 18 | const ( 19 | UnknownState HandshakeState = iota 20 | Status 21 | Login 22 | ) 23 | 24 | func RequestState(n byte) HandshakeState { 25 | var t HandshakeState 26 | switch n { 27 | case 1: 28 | t = Status 29 | case 2: 30 | t = Login 31 | default: 32 | t = UnknownState 33 | } 34 | return t 35 | } 36 | 37 | type McTypesHandshake struct { 38 | ProtocolVersion VarInt 39 | ServerAddress String 40 | ServerPort UnsignedShort 41 | NextState VarInt 42 | } 43 | 44 | type ServerBoundHandshake struct { 45 | ProtocolVersion int 46 | ServerAddress string 47 | ServerPort int16 48 | NextState byte 49 | } 50 | 51 | func (pk ServerBoundHandshake) Marshal() Packet { 52 | return MarshalPacket( 53 | ServerBoundHandshakePacketID, 54 | VarInt(pk.ProtocolVersion), 55 | String(pk.ServerAddress), 56 | UnsignedShort(pk.ServerPort), 57 | VarInt(pk.NextState), 58 | ) 59 | } 60 | 61 | func (pk ServerBoundHandshake) MarshalPacket() Packet { 62 | return pk.Marshal() 63 | } 64 | 65 | func UnmarshalServerBoundHandshake(packet Packet) (ServerBoundHandshake, error) { 66 | var pk McTypesHandshake 67 | var hs ServerBoundHandshake 68 | 69 | if packet.ID != ServerBoundHandshakePacketID { 70 | return hs, ErrInvalidPacketID 71 | } 72 | 73 | if err := packet.Scan( 74 | &pk.ProtocolVersion, 75 | &pk.ServerAddress, 76 | &pk.ServerPort, 77 | &pk.NextState, 78 | ); err != nil { 79 | return hs, err 80 | } 81 | 82 | hs = ServerBoundHandshake{ 83 | ProtocolVersion: int(pk.ProtocolVersion), 84 | ServerAddress: string(pk.ServerAddress), 85 | ServerPort: int16(pk.ServerPort), 86 | NextState: byte(pk.NextState), 87 | } 88 | return hs, nil 89 | } 90 | 91 | func UnmarshalServerBoundHandshake2(packet Packet) (ServerBoundHandshake, error) { 92 | var hs ServerBoundHandshake 93 | 94 | if packet.ID != ServerBoundHandshakePacketID { 95 | return hs, ErrInvalidPacketID 96 | } 97 | 98 | buf := bytes.NewBuffer(packet.Data) 99 | var err error 100 | hs.ProtocolVersion, err = ReadVarInt_ByteReader(buf) 101 | if err != nil { 102 | return hs, err 103 | } 104 | hs.ServerAddress, err = ReadString_ByteReader(buf) 105 | if err != nil { 106 | return hs, err 107 | } 108 | hs.ServerPort, err = ReadShot_ByteReader(buf) 109 | if err != nil { 110 | return hs, err 111 | } 112 | state, err := ReadVarInt_ByteReader(buf) 113 | if err != nil { 114 | return hs, err 115 | } 116 | hs.NextState = byte(state) 117 | return hs, nil 118 | } 119 | 120 | func UnmarshalServerBoundHandshake_ByteReader(r io.ByteReader) (ServerBoundHandshake, error) { 121 | var hs ServerBoundHandshake 122 | packetID, err := r.ReadByte() 123 | if err != nil { 124 | return hs, err 125 | } 126 | if packetID != ServerBoundHandshakePacketID { 127 | return hs, ErrInvalidPacketID 128 | } 129 | 130 | hs.ProtocolVersion, err = ReadVarInt_ByteReader(r) 131 | if err != nil { 132 | return hs, err 133 | } 134 | hs.ServerAddress, err = ReadString_ByteReader(r) 135 | if err != nil { 136 | return hs, err 137 | } 138 | hs.ServerPort, err = ReadShot_ByteReader(r) 139 | if err != nil { 140 | return hs, err 141 | } 142 | state, err := ReadVarInt_ByteReader(r) 143 | if err != nil { 144 | return hs, err 145 | } 146 | hs.NextState = byte(state) 147 | return hs, nil 148 | } 149 | 150 | func (hs ServerBoundHandshake) State() HandshakeState { 151 | var state HandshakeState 152 | switch hs.NextState { 153 | case 1: 154 | state = Status 155 | case 2: 156 | state = Login 157 | default: 158 | state = UnknownState 159 | } 160 | return state 161 | } 162 | 163 | func (hs ServerBoundHandshake) IsStatusRequest() bool { 164 | return VarInt(hs.NextState) == HandshakeStatusState 165 | } 166 | 167 | func (hs ServerBoundHandshake) IsLoginRequest() bool { 168 | return VarInt(hs.NextState) == HandshakeLoginState 169 | } 170 | 171 | func (hs ServerBoundHandshake) IsForgeAddress() bool { 172 | addr := string(hs.ServerAddress) 173 | return len(strings.Split(addr, ForgeSeparator)) > 1 174 | } 175 | 176 | func (hs ServerBoundHandshake) IsRealIPAddress() bool { 177 | addr := string(hs.ServerAddress) 178 | return len(strings.Split(addr, RealIPSeparator)) > 1 179 | } 180 | 181 | func (hs ServerBoundHandshake) ParseServerAddress() string { 182 | addr := hs.ServerAddress 183 | addr = strings.Split(addr, ForgeSeparator)[0] 184 | addr = strings.Split(addr, RealIPSeparator)[0] 185 | return addr 186 | } 187 | 188 | func (hs *ServerBoundHandshake) UpgradeToOldRealIP(clientAddr string) { 189 | hs.UpgradeToOldRealIP_WithTime(clientAddr, time.Now()) 190 | } 191 | 192 | func (hs *ServerBoundHandshake) UpgradeToOldRealIP_WithTime(clientAddr string, stamp time.Time) { 193 | if hs.IsRealIPAddress() { 194 | return 195 | } 196 | 197 | addr := string(hs.ServerAddress) 198 | addrWithForge := strings.SplitN(addr, ForgeSeparator, 3) 199 | 200 | addr = fmt.Sprintf("%s///%s///%d", addrWithForge[0], clientAddr, stamp.Unix()) 201 | 202 | if len(addrWithForge) > 1 { 203 | addr = fmt.Sprintf("%s\x00%s\x00", addr, addrWithForge[1]) 204 | } 205 | 206 | hs.ServerAddress = addr 207 | } 208 | 209 | func (hs *ServerBoundHandshake) UpgradeToNewRealIP(clientAddr string, key *ecdsa.PrivateKey) error { 210 | hs.UpgradeToOldRealIP(clientAddr) 211 | text := hs.ServerAddress 212 | hash := sha512.Sum512([]byte(text)) 213 | bytes, err := ecdsa.SignASN1(rand.Reader, key, hash[:]) 214 | if err != nil { 215 | return err 216 | } 217 | encoded := base64.StdEncoding.EncodeToString(bytes) 218 | addr := fmt.Sprintf("%s///%s", hs.ServerAddress, encoded) 219 | hs.ServerAddress = addr 220 | return nil 221 | } 222 | 223 | const ServerBoundLoginStartPacketID byte = 0x00 224 | 225 | type ServerLoginStart struct { 226 | Name String 227 | } 228 | 229 | func (pk ServerLoginStart) Marshal() Packet { 230 | return MarshalPacket(ServerBoundLoginStartPacketID, pk.Name) 231 | } 232 | 233 | func (pk *ServerLoginStart) MarshalPacket() Packet { 234 | return pk.Marshal() 235 | } 236 | 237 | func UnmarshalServerBoundLoginStart(packet Packet) (ServerLoginStart, error) { 238 | var pk ServerLoginStart 239 | 240 | if packet.ID != ServerBoundLoginStartPacketID { 241 | return pk, ErrInvalidPacketID 242 | } 243 | 244 | if err := packet.Scan(&pk.Name); err != nil { 245 | return pk, err 246 | } 247 | 248 | return pk, nil 249 | } 250 | 251 | const ClientBoundDisconnectPacketID byte = 0x00 252 | 253 | type DisconnectClient struct { 254 | Reason string `json:"reason,omitempty"` 255 | } 256 | 257 | func (pk DisconnectClient) Marshal() Packet { 258 | return MarshalPacket( 259 | ClientBoundDisconnectPacketID, 260 | Chat(pk.Reason), 261 | ) 262 | } 263 | 264 | func (pk *DisconnectClient) MarshalPacket() Packet { 265 | return pk.Marshal() 266 | } 267 | 268 | type ClientBoundDisconnect struct { 269 | Reason Chat 270 | } 271 | 272 | func (pk ClientBoundDisconnect) Marshal() Packet { 273 | return MarshalPacket( 274 | ClientBoundDisconnectPacketID, 275 | pk.Reason, 276 | ) 277 | } 278 | 279 | func (pk *ClientBoundDisconnect) MarshalPacket() Packet { 280 | return pk.Marshal() 281 | } 282 | 283 | func UnmarshalClientDisconnect(packet Packet) (ClientBoundDisconnect, error) { 284 | var pk ClientBoundDisconnect 285 | 286 | if packet.ID != ClientBoundDisconnectPacketID { 287 | return pk, ErrInvalidPacketID 288 | } 289 | 290 | err := packet.Scan(&pk.Reason) 291 | return pk, err 292 | } 293 | -------------------------------------------------------------------------------- /mc/packet_status.go: -------------------------------------------------------------------------------- 1 | package mc 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const ( 10 | ClientBoundResponsePacketID byte = 0x00 11 | ServerBoundRequestPacketID byte = 0x00 12 | ServerBoundPingPacketID byte = 0x01 13 | ClientBoundPongPacketID byte = 0x01 14 | ) 15 | 16 | type SimpleStatus struct { 17 | Name string `json:"name"` 18 | Protocol int `json:"protocol"` 19 | Description string `json:"text"` 20 | Favicon string `json:"favicon,omitempty"` 21 | } 22 | 23 | func (pk SimpleStatus) Marshal() Packet { 24 | var favicon string 25 | if pk.Favicon != "" { 26 | favicon = fmt.Sprintf("data:image/png;base64,%s", pk.Favicon) 27 | } 28 | jsonResponse := ResponseJSON{ 29 | Version: VersionJSON{ 30 | Name: pk.Name, 31 | Protocol: pk.Protocol, 32 | }, 33 | Description: DescriptionJSON{ 34 | Text: pk.Description, 35 | }, 36 | Favicon: favicon, 37 | } 38 | text, _ := json.Marshal(jsonResponse) 39 | response := ClientBoundResponse{ 40 | JSONResponse: String(text), 41 | } 42 | return response.Marshal() 43 | } 44 | 45 | func (pk *SimpleStatus) MarshalPacket() Packet { 46 | return pk.Marshal() 47 | } 48 | 49 | type DifferentStatusResponse struct { 50 | Version VersionJSON `json:"version"` 51 | Description DescriptionJSON `json:"description"` 52 | Favicon string `json:"favicon,omitempty"` 53 | } 54 | 55 | func (pk *DifferentStatusResponse) Marshal() Packet { 56 | jsonResponse := ResponseJSON{ 57 | Version: pk.Version, 58 | Description: pk.Description, 59 | Favicon: pk.Favicon, 60 | } 61 | text, _ := json.Marshal(jsonResponse) 62 | response := ClientBoundResponse{ 63 | JSONResponse: String(text), 64 | } 65 | return response.Marshal() 66 | } 67 | 68 | type ClientBoundResponse struct { 69 | JSONResponse String 70 | } 71 | 72 | func (pk *ClientBoundResponse) Marshal() Packet { 73 | return MarshalPacket( 74 | ClientBoundResponsePacketID, 75 | pk.JSONResponse, 76 | ) 77 | } 78 | 79 | func UnmarshalClientBoundResponse(packet Packet) (ClientBoundResponse, error) { 80 | var pk ClientBoundResponse 81 | 82 | if packet.ID != ClientBoundResponsePacketID { 83 | return pk, ErrInvalidPacketID 84 | } 85 | 86 | if err := packet.Scan( 87 | &pk.JSONResponse, 88 | ); err != nil { 89 | return pk, err 90 | } 91 | 92 | return pk, nil 93 | } 94 | 95 | type ResponseJSON struct { 96 | Version VersionJSON `json:"version"` 97 | Players PlayersJSON `json:"players"` 98 | Description DescriptionJSON `json:"description"` 99 | Favicon string `json:"favicon"` 100 | } 101 | 102 | type VersionJSON struct { 103 | Name string `json:"name"` 104 | Protocol int `json:"protocol"` 105 | } 106 | 107 | type PlayersJSON struct { 108 | Max int `json:"max"` 109 | Online int `json:"online"` 110 | Sample []PlayerSampleJSON `json:"sample"` 111 | } 112 | 113 | type PlayerSampleJSON struct { 114 | Name string `json:"name"` 115 | ID string `json:"id"` 116 | } 117 | 118 | type DescriptionJSON struct { 119 | Text string `json:"text"` 120 | } 121 | 122 | type ServerBoundRequest struct{} 123 | 124 | func ServerBoundRequestPacket() Packet { 125 | return Packet{ID: ServerBoundRequestPacketID} 126 | } 127 | 128 | func (pk ServerBoundRequest) Marshal() Packet { 129 | return MarshalPacket( 130 | ServerBoundRequestPacketID, 131 | ) 132 | } 133 | 134 | func (pk *ServerBoundRequest) MarshalPacket() Packet { 135 | return pk.Marshal() 136 | } 137 | 138 | func NewServerBoundPing() ServerBoundPing { 139 | millisecondTime := time.Now().UnixNano() / 1e6 140 | return ServerBoundPing{ 141 | Time: Long(millisecondTime), 142 | } 143 | } 144 | 145 | type ServerBoundPing struct { 146 | Time Long 147 | } 148 | 149 | func (pk ServerBoundPing) Marshal() Packet { 150 | return MarshalPacket( 151 | ServerBoundPingPacketID, 152 | pk.Time, 153 | ) 154 | } 155 | -------------------------------------------------------------------------------- /mc/packet_status_test.go: -------------------------------------------------------------------------------- 1 | package mc_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/realDragonium/Ultraviolet/mc" 8 | ) 9 | 10 | func TestClientBoundResponse_Marshal(t *testing.T) { 11 | tt := []struct { 12 | packet mc.ClientBoundResponse 13 | marshaledPacket mc.Packet 14 | }{ 15 | { 16 | packet: mc.ClientBoundResponse{ 17 | JSONResponse: mc.String(""), 18 | }, 19 | marshaledPacket: mc.Packet{ 20 | ID: 0x00, 21 | Data: []byte{0x00}, 22 | }, 23 | }, 24 | { 25 | packet: mc.ClientBoundResponse{ 26 | JSONResponse: mc.String("Hello, World!"), 27 | }, 28 | marshaledPacket: mc.Packet{ 29 | ID: 0x00, 30 | Data: []byte{0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21}, 31 | }, 32 | }, 33 | } 34 | 35 | for _, tc := range tt { 36 | pk := tc.packet.Marshal() 37 | 38 | if pk.ID != mc.ClientBoundResponsePacketID { 39 | t.Error("invalid packet id") 40 | } 41 | 42 | if !bytes.Equal(pk.Data, tc.marshaledPacket.Data) { 43 | t.Errorf("got: %v, want: %v", pk.Data, tc.marshaledPacket.Data) 44 | } 45 | } 46 | } 47 | 48 | func TestUnmarshalClientBoundResponse(t *testing.T) { 49 | tt := []struct { 50 | packet mc.Packet 51 | unmarshalledPacket mc.ClientBoundResponse 52 | }{ 53 | { 54 | packet: mc.Packet{ 55 | ID: 0x00, 56 | Data: []byte{0x00}, 57 | }, 58 | unmarshalledPacket: mc.ClientBoundResponse{ 59 | JSONResponse: "", 60 | }, 61 | }, 62 | { 63 | packet: mc.Packet{ 64 | ID: 0x00, 65 | Data: []byte{0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21}, 66 | }, 67 | unmarshalledPacket: mc.ClientBoundResponse{ 68 | JSONResponse: mc.String("Hello, World!"), 69 | }, 70 | }, 71 | } 72 | 73 | for _, tc := range tt { 74 | actual, err := mc.UnmarshalClientBoundResponse(tc.packet) 75 | if err != nil { 76 | t.Error(err) 77 | } 78 | 79 | expected := tc.unmarshalledPacket 80 | 81 | if actual.JSONResponse != expected.JSONResponse { 82 | t.Errorf("got: %v, want: %v", actual, expected) 83 | } 84 | } 85 | 86 | } 87 | 88 | func TestServerBoundRequest_Marshal(t *testing.T) { 89 | tt := []struct { 90 | packet mc.ServerBoundRequest 91 | marshaledPacket mc.Packet 92 | }{ 93 | { 94 | packet: mc.ServerBoundRequest{}, 95 | marshaledPacket: mc.Packet{ 96 | ID: 0x00, 97 | Data: []byte{}, 98 | }, 99 | }, 100 | } 101 | 102 | for _, tc := range tt { 103 | pk := tc.packet.Marshal() 104 | 105 | if pk.ID != mc.ServerBoundRequestPacketID { 106 | t.Error("invalid packet id") 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /mc/packet_test.go: -------------------------------------------------------------------------------- 1 | package mc_test 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "net" 7 | "testing" 8 | 9 | "github.com/realDragonium/Ultraviolet/mc" 10 | ) 11 | 12 | func TestPacket_Marshal(t *testing.T) { 13 | tt := []struct { 14 | packet mc.Packet 15 | expected []byte 16 | expectError error 17 | }{ 18 | { 19 | packet: mc.Packet{ 20 | ID: 0x00, 21 | Data: []byte{0x00, 0xf2}, 22 | }, 23 | expected: []byte{0x03, 0x00, 0x00, 0xf2}, 24 | }, 25 | { 26 | packet: mc.Packet{ 27 | ID: 0x0f, 28 | Data: []byte{0x00, 0xf2, 0x03, 0x50}, 29 | }, 30 | expected: []byte{0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50}, 31 | }, 32 | // { 33 | // packet: mc.Packet{ 34 | // ID: 0x0f, 35 | // Data: make([]byte, 0xffffff), 36 | // }, 37 | // expectError: mc.ErrVarIntSize, 38 | // }, 39 | } 40 | 41 | for _, tc := range tt { 42 | actual := tc.packet.Marshal() 43 | if !bytes.Equal(actual, tc.expected) { 44 | t.Errorf("got: %v; want: %v", actual, tc.expected) 45 | } 46 | } 47 | } 48 | 49 | func TestPacket_Scan(t *testing.T) { 50 | packet := mc.Packet{ 51 | ID: 0x00, 52 | Data: []byte{0xf2}, 53 | } 54 | 55 | var byteField mc.Byte 56 | 57 | err := packet.Scan( 58 | &byteField, 59 | ) 60 | 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | 65 | if !bytes.Equal(byteField.Encode(), []byte{0xf2}) { 66 | t.Errorf("got: %x; want: %x", byteField.Encode(), 0xf2) 67 | } 68 | } 69 | 70 | func TestScanFields(t *testing.T) { 71 | packet := mc.Packet{ 72 | ID: 0x00, 73 | Data: []byte{0xf2}, 74 | } 75 | 76 | var byteField mc.Byte 77 | 78 | err := mc.ScanFields( 79 | bytes.NewReader(packet.Data), 80 | &byteField, 81 | ) 82 | 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | 87 | if !bytes.Equal(byteField.Encode(), []byte{0xf2}) { 88 | t.Errorf("got: %x; want: %x", byteField.Encode(), 0xf2) 89 | } 90 | } 91 | 92 | func TestMarshalPacket(t *testing.T) { 93 | packetId := byte(0x00) 94 | byteField := mc.Byte(0x0f) 95 | packetData := []byte{0x0f} 96 | 97 | packet := mc.MarshalPacket(packetId, byteField) 98 | 99 | if packet.ID != packetId { 100 | t.Errorf("packet id: got: %v; want: %v", packet.ID, packetId) 101 | } 102 | 103 | if !bytes.Equal(packet.Data, packetData) { 104 | t.Errorf("got: %v; want: %v", packet.Data, packetData) 105 | } 106 | } 107 | 108 | func TestReadPacketBytes(t *testing.T) { 109 | tt := []struct { 110 | data []byte 111 | packetBytes []byte 112 | }{ 113 | { 114 | data: []byte{0x03, 0x00, 0x00, 0xf2, 0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50}, 115 | packetBytes: []byte{0x00, 0x00, 0xf2}, 116 | }, 117 | { 118 | data: []byte{0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50, 0x30, 0x01, 0xef, 0xaa}, 119 | packetBytes: []byte{0x0f, 0x00, 0xf2, 0x03, 0x50}, 120 | }, 121 | } 122 | 123 | for _, tc := range tt { 124 | readBytes, err := mc.ReadPacketBytes(bytes.NewReader(tc.data)) 125 | if err != nil { 126 | t.Error(err) 127 | } 128 | 129 | if !bytes.Equal(readBytes, tc.packetBytes) { 130 | t.Errorf("got: %v; want: %v", readBytes, tc.packetBytes) 131 | } 132 | } 133 | } 134 | 135 | func TestReadPacket(t *testing.T) { 136 | tt := []struct { 137 | data []byte 138 | packet mc.Packet 139 | dataAfterRead []byte 140 | }{ 141 | { 142 | data: []byte{0x03, 0x00, 0x00, 0xf2, 0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50}, 143 | packet: mc.Packet{ 144 | ID: 0x00, 145 | Data: []byte{0x00, 0xf2}, 146 | }, 147 | dataAfterRead: []byte{0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50}, 148 | }, 149 | { 150 | data: []byte{0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50, 0x30, 0x01, 0xef, 0xaa}, 151 | packet: mc.Packet{ 152 | ID: 0x0f, 153 | Data: []byte{0x00, 0xf2, 0x03, 0x50}, 154 | }, 155 | dataAfterRead: []byte{0x30, 0x01, 0xef, 0xaa}, 156 | }, 157 | } 158 | 159 | for _, tc := range tt { 160 | buf := bytes.NewBuffer(tc.data) 161 | pk, err := mc.ReadPacketOld(buf) 162 | if err != nil { 163 | t.Error(err) 164 | } 165 | 166 | if pk.ID != tc.packet.ID { 167 | t.Errorf("packet ID: got: %v; want: %v", pk.ID, tc.packet.ID) 168 | } 169 | 170 | if !bytes.Equal(pk.Data, tc.packet.Data) { 171 | t.Errorf("packet data: got: %v; want: %v", pk.Data, tc.packet.Data) 172 | } 173 | 174 | if !bytes.Equal(buf.Bytes(), tc.dataAfterRead) { 175 | t.Errorf("data after read: got: %v; want: %v", tc.data, tc.dataAfterRead) 176 | } 177 | } 178 | } 179 | 180 | func benchmarkReadPacker(b *testing.B, amountBytes int) { 181 | data := []byte{} 182 | 183 | for i := 0; i < amountBytes; i++ { 184 | data = append(data, 1) 185 | } 186 | pk := mc.Packet{ID: 0x05, Data: data} 187 | bytes := pk.Marshal() 188 | c1, c2 := net.Pipe() 189 | r := bufio.NewReader(c1) 190 | 191 | go func() { 192 | for { 193 | c2.Write(bytes) 194 | } 195 | }() 196 | 197 | for n := 0; n < b.N; n++ { 198 | if _, err := mc.ReadPacketOld(r); err != nil { 199 | b.Error(err) 200 | } 201 | } 202 | 203 | } 204 | 205 | func BenchmarkReadPacker_SingleByteVarInt(b *testing.B) { 206 | size := 0b0101111 207 | benchmarkReadPacker(b, size) 208 | } 209 | 210 | func BenchmarkReadPacker_DoubleByteVarInt(b *testing.B) { 211 | size := 0b1111111_0101111 212 | benchmarkReadPacker(b, size) 213 | } 214 | 215 | func BenchmarkReadPacker_TripleByteVarInt(b *testing.B) { 216 | size := 0b1111111_1111111_0101111 217 | benchmarkReadPacker(b, size) 218 | } 219 | -------------------------------------------------------------------------------- /mc/type.go: -------------------------------------------------------------------------------- 1 | package mc 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | var ( 9 | ErrVarIntSize = errors.New("VarInt is too big") 10 | ) 11 | 12 | // A Field is both FieldEncoder and FieldDecoder 13 | type Field interface { 14 | FieldEncoder 15 | FieldDecoder 16 | } 17 | 18 | // A FieldEncoder can be encode as minecraft protocol used. 19 | type FieldEncoder interface { 20 | Encode() []byte 21 | } 22 | 23 | // A FieldDecoder can Decode from minecraft protocol 24 | type FieldDecoder interface { 25 | Decode(r DecodeReader) error 26 | } 27 | 28 | //DecodeReader is both io.Reader and io.ByteReader 29 | type DecodeReader interface { 30 | io.ByteReader 31 | io.Reader 32 | } 33 | 34 | type ( 35 | // Byte is signed 8-bit integer, two's complement 36 | Byte int8 37 | // UnsignedShort is unsigned 16-bit integer 38 | UnsignedShort uint16 39 | // Long is signed 64-bit integer, two's complement 40 | Long int64 41 | // String is sequence of Unicode scalar values with a max length of 32767 42 | String string 43 | // Chat is encoded as a String with max length of 262144. 44 | Chat = String 45 | // VarInt is variable-length data encoding a two's complement signed 32-bit integer 46 | VarInt int32 47 | ) 48 | 49 | // ReadNBytes read N bytes from bytes.Reader 50 | func ReadNBytes(r DecodeReader, n int) ([]byte, error) { 51 | bb := make([]byte, n) 52 | var err error 53 | for i := 0; i < n; i++ { 54 | bb[i], err = r.ReadByte() 55 | if err != nil { 56 | return nil, err 57 | } 58 | } 59 | return bb, nil 60 | } 61 | 62 | // Encode a String 63 | func (s String) Encode() []byte { 64 | byteString := []byte(s) 65 | var bb []byte 66 | bb = append(bb, VarInt(len(byteString)).Encode()...) // len 67 | bb = append(bb, byteString...) // data 68 | return bb 69 | } 70 | 71 | // Decode a String 72 | func (s *String) Decode(r DecodeReader) error { 73 | var l VarInt // String length 74 | if err := l.Decode(r); err != nil { 75 | return err 76 | } 77 | 78 | bb, err := ReadNBytes(r, int(l)) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | *s = String(bb) 84 | return nil 85 | } 86 | 87 | // Encode a Byte 88 | func (b Byte) Encode() []byte { 89 | return []byte{byte(b)} 90 | } 91 | 92 | // Decode a Byte 93 | func (b *Byte) Decode(r DecodeReader) error { 94 | v, err := r.ReadByte() 95 | if err != nil { 96 | return err 97 | } 98 | *b = Byte(v) 99 | return nil 100 | } 101 | 102 | // Encode a Unsigned Short 103 | func (us UnsignedShort) Encode() []byte { 104 | n := uint16(us) 105 | return []byte{ 106 | byte(n >> 8), 107 | byte(n), 108 | } 109 | } 110 | 111 | // Decode a UnsignedShort 112 | func (us *UnsignedShort) Decode(r DecodeReader) error { 113 | bb, err := ReadNBytes(r, 2) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | *us = UnsignedShort(int16(bb[0])<<8 | int16(bb[1])) 119 | return nil 120 | } 121 | 122 | // Encode a Long 123 | func (l Long) Encode() []byte { 124 | n := uint64(l) 125 | return []byte{ 126 | byte(n >> 56), byte(n >> 48), byte(n >> 40), byte(n >> 32), 127 | byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n), 128 | } 129 | } 130 | 131 | // Decode a Long 132 | func (l *Long) Decode(r DecodeReader) error { 133 | bb, err := ReadNBytes(r, 8) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | *l = Long(int64(bb[0])<<56 | int64(bb[1])<<48 | int64(bb[2])<<40 | int64(bb[3])<<32 | 139 | int64(bb[4])<<24 | int64(bb[5])<<16 | int64(bb[6])<<8 | int64(bb[7])) 140 | return nil 141 | } 142 | 143 | // Encode a VarInt 144 | func (v VarInt) Encode() []byte { 145 | num := uint32(v) 146 | var bb []byte 147 | for { 148 | b := num & 0x7F 149 | num >>= 7 150 | if num != 0 { 151 | b |= 0x80 152 | } 153 | bb = append(bb, byte(b)) 154 | if num == 0 { 155 | break 156 | } 157 | } 158 | return bb 159 | } 160 | 161 | // Decode a VarInt 162 | func (v *VarInt) Decode(r DecodeReader) error { 163 | var n uint32 164 | for i := 0; ; i++ { 165 | sec, err := r.ReadByte() 166 | if err != nil { 167 | return err 168 | } 169 | 170 | n |= uint32(sec&0x7F) << uint32(7*i) 171 | 172 | if i >= 5 { 173 | return ErrVarIntSize 174 | } else if sec&0x80 == 0 { 175 | break 176 | } 177 | } 178 | 179 | *v = VarInt(n) 180 | return nil 181 | } 182 | 183 | // Read a VarInt 184 | func ReadVarInt(r DecodeReader) (VarInt, error) { 185 | var n uint32 186 | for i := 0; ; i++ { 187 | sec, err := r.ReadByte() 188 | if err != nil { 189 | return 0, err 190 | } 191 | n |= uint32(sec&0x7F) << uint32(7*i) 192 | if i >= 5 { 193 | return 0, ErrVarIntSize 194 | } else if sec&0x80 == 0 { 195 | break 196 | } 197 | } 198 | return VarInt(n), nil 199 | } 200 | 201 | // Read a UnsignedShort 202 | func ReadUnsignedShort(r DecodeReader) (UnsignedShort, error) { 203 | bb, err := ReadNBytes(r, 2) 204 | if err != nil { 205 | return 0, err 206 | } 207 | return UnsignedShort(int16(bb[0])<<8 | int16(bb[1])), nil 208 | } 209 | 210 | // Read a String 211 | func ReadString(r DecodeReader) (String, error) { 212 | l, err := ReadVarInt(r) // String length 213 | if err != nil { 214 | return "", err 215 | } 216 | bb, err := ReadNBytes(r, int(l)) 217 | if err != nil { 218 | return "", err 219 | } 220 | return String(bb), nil 221 | } 222 | 223 | // Read a Byte 224 | func ReadByte(r DecodeReader) (Byte, error) { 225 | v, err := r.ReadByte() 226 | if err != nil { 227 | return 0, err 228 | } 229 | return Byte(v), nil 230 | } 231 | 232 | func ReadPacketSize_Bytes(bytes []byte) (int, int, error) { 233 | var n int32 234 | var i int 235 | for i = 0; ; i++ { 236 | sec := bytes[i] 237 | n |= int32(sec&0x7F) << uint32(7*i) 238 | if i >= 5 { 239 | return 0, 0, ErrVarIntSize 240 | } else if sec&0x80 == 0 { 241 | break 242 | } 243 | } 244 | return int(n), i, nil 245 | } 246 | 247 | func ReadNBytes_ByteReader(r io.ByteReader, n int) ([]byte, error) { 248 | bb := make([]byte, n) 249 | var err error 250 | for i := 0; i < n; i++ { 251 | bb[i], err = r.ReadByte() 252 | if err != nil { 253 | return nil, err 254 | } 255 | } 256 | return bb, nil 257 | } 258 | 259 | func ReadVarInt_ByteReader(b io.ByteReader) (int, error) { 260 | var n uint32 261 | 262 | for i := 0; ; i++ { 263 | sec, err := b.ReadByte() 264 | if err != nil { 265 | return 0, err 266 | } 267 | n |= uint32(sec&0x7F) << uint32(7*i) 268 | if i >= 5 { 269 | return 0, ErrVarIntSize 270 | } else if sec&0x80 == 0 { 271 | break 272 | } 273 | } 274 | return int(n), nil 275 | } 276 | 277 | // Decode a String 278 | func ReadString_ByteReader(r io.ByteReader) (string, error) { 279 | length, err := ReadVarInt_ByteReader(r) 280 | if err != nil { 281 | return "", err 282 | } 283 | 284 | bb, err := ReadNBytes_ByteReader(r, length) 285 | if err != nil { 286 | return "", err 287 | } 288 | 289 | return string(bb), nil 290 | } 291 | 292 | // Decode a UnsignedShort 293 | func ReadShot_ByteReader(r io.ByteReader) (int16, error) { 294 | bb, err := ReadNBytes_ByteReader(r, 2) 295 | if err != nil { 296 | return 0, err 297 | } 298 | return (int16(bb[0])<<8 | int16(bb[1])), nil 299 | } 300 | -------------------------------------------------------------------------------- /mc/type_test.go: -------------------------------------------------------------------------------- 1 | package mc_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/realDragonium/Ultraviolet/mc" 10 | ) 11 | 12 | func TestReadNBytes(t *testing.T) { 13 | tt := [][]byte{ 14 | {0x00, 0x01, 0x02, 0x03}, 15 | {0x03, 0x01, 0x02, 0x02}, 16 | } 17 | 18 | for _, tc := range tt { 19 | bb, err := mc.ReadNBytes(bytes.NewBuffer(tc), len(tc)) 20 | if err != nil { 21 | t.Errorf("reading bytes: %s", err) 22 | } 23 | 24 | if !bytes.Equal(bb, tc) { 25 | t.Errorf("got %v; want: %v", bb, tc) 26 | } 27 | } 28 | } 29 | 30 | func TestVarInt(t *testing.T) { 31 | tt := []struct { 32 | decoded mc.VarInt 33 | encoded []byte 34 | }{ 35 | { 36 | decoded: mc.VarInt(0), 37 | encoded: []byte{0x00}, 38 | }, 39 | { 40 | decoded: mc.VarInt(1), 41 | encoded: []byte{0x01}, 42 | }, 43 | { 44 | decoded: mc.VarInt(2), 45 | encoded: []byte{0x02}, 46 | }, 47 | { 48 | decoded: mc.VarInt(127), 49 | encoded: []byte{0x7f}, 50 | }, 51 | { 52 | decoded: mc.VarInt(128), 53 | encoded: []byte{0x80, 0x01}, 54 | }, 55 | { 56 | decoded: mc.VarInt(255), 57 | encoded: []byte{0xff, 0x01}, 58 | }, 59 | { 60 | decoded: mc.VarInt(2097151), 61 | encoded: []byte{0xff, 0xff, 0x7f}, 62 | }, 63 | { 64 | decoded: mc.VarInt(2147483647), 65 | encoded: []byte{0xff, 0xff, 0xff, 0xff, 0x07}, 66 | }, 67 | { 68 | decoded: mc.VarInt(-1), 69 | encoded: []byte{0xff, 0xff, 0xff, 0xff, 0x0f}, 70 | }, 71 | { 72 | decoded: mc.VarInt(-2147483648), 73 | encoded: []byte{0x80, 0x80, 0x80, 0x80, 0x08}, 74 | }, 75 | } 76 | 77 | t.Run("encode", func(t *testing.T) { 78 | for _, tc := range tt { 79 | if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { 80 | t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) 81 | } 82 | } 83 | }) 84 | 85 | t.Run("decode", func(t *testing.T) { 86 | for _, tc := range tt { 87 | var actualDecoded mc.VarInt 88 | if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { 89 | t.Errorf("decoding: %s", err) 90 | } 91 | 92 | if actualDecoded != tc.decoded { 93 | t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) 94 | } 95 | } 96 | }) 97 | } 98 | 99 | func TestString(t *testing.T) { 100 | tt := []struct { 101 | decoded mc.String 102 | encoded []byte 103 | }{ 104 | { 105 | decoded: mc.String(""), 106 | encoded: []byte{0x00}, 107 | }, 108 | { 109 | decoded: mc.String("Hello, World!"), 110 | encoded: []byte{0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21}, 111 | }, 112 | { 113 | decoded: mc.String("Minecraft"), 114 | encoded: []byte{0x09, 0x4d, 0x69, 0x6e, 0x65, 0x63, 0x72, 0x61, 0x66, 0x74}, 115 | }, 116 | { 117 | decoded: mc.String("♥"), 118 | encoded: []byte{0x03, 0xe2, 0x99, 0xa5}, 119 | }, 120 | { 121 | decoded: mc.String("{\"text\": \"Please reconnect to verify yourself\"}"), 122 | encoded: []byte{47, 123, 34, 116, 101, 120, 116, 34, 58, 32, 34, 80, 108, 101, 97, 115, 101, 32, 114, 101, 99, 111, 110, 110, 101, 99, 116, 32, 116, 111, 32, 118, 101, 114, 105, 102, 121, 32, 121, 111, 117, 114, 115, 101, 108, 102, 34, 125}, 123 | }, 124 | } 125 | 126 | t.Run("encode", func(t *testing.T) { 127 | for _, tc := range tt { 128 | if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { 129 | t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) 130 | } 131 | } 132 | }) 133 | 134 | t.Run("decode", func(t *testing.T) { 135 | for _, tc := range tt { 136 | var actualDecoded mc.String 137 | if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { 138 | t.Errorf("decoding: %s", err) 139 | } 140 | 141 | if actualDecoded != tc.decoded { 142 | t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) 143 | } 144 | } 145 | }) 146 | 147 | } 148 | 149 | func TestByte(t *testing.T) { 150 | tt := []struct { 151 | decoded mc.Byte 152 | encoded []byte 153 | }{ 154 | { 155 | decoded: mc.Byte(0x00), 156 | encoded: []byte{0x00}, 157 | }, 158 | { 159 | decoded: mc.Byte(0x0f), 160 | encoded: []byte{0x0f}, 161 | }, 162 | } 163 | 164 | t.Run("encode", func(t *testing.T) { 165 | for _, tc := range tt { 166 | if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { 167 | t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) 168 | } 169 | } 170 | }) 171 | 172 | t.Run("decode", func(t *testing.T) { 173 | for _, tc := range tt { 174 | var actualDecoded mc.Byte 175 | if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { 176 | t.Errorf("decoding: %s", err) 177 | } 178 | 179 | if actualDecoded != tc.decoded { 180 | t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) 181 | } 182 | } 183 | }) 184 | } 185 | func TestUnsignedShort(t *testing.T) { 186 | tt := []struct { 187 | decoded mc.UnsignedShort 188 | encoded []byte 189 | }{ 190 | { 191 | decoded: mc.UnsignedShort(0), 192 | encoded: []byte{0x00, 0x00}, 193 | }, 194 | { 195 | decoded: mc.UnsignedShort(15), 196 | encoded: []byte{0x00, 0x0f}, 197 | }, 198 | { 199 | decoded: mc.UnsignedShort(16), 200 | encoded: []byte{0x00, 0x10}, 201 | }, 202 | { 203 | decoded: mc.UnsignedShort(255), 204 | encoded: []byte{0x00, 0xff}, 205 | }, 206 | { 207 | decoded: mc.UnsignedShort(256), 208 | encoded: []byte{0x01, 0x00}, 209 | }, 210 | { 211 | decoded: mc.UnsignedShort(65535), 212 | encoded: []byte{0xff, 0xff}, 213 | }, 214 | } 215 | 216 | t.Run("encode", func(t *testing.T) { 217 | for _, tc := range tt { 218 | if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { 219 | t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) 220 | } 221 | } 222 | }) 223 | 224 | t.Run("decode", func(t *testing.T) { 225 | for _, tc := range tt { 226 | var actualDecoded mc.UnsignedShort 227 | if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { 228 | t.Errorf("decoding: %s", err) 229 | } 230 | 231 | if actualDecoded != tc.decoded { 232 | t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) 233 | } 234 | } 235 | }) 236 | } 237 | 238 | func TestReadVarInt(t *testing.T) { 239 | tt := []struct { 240 | decoded mc.VarInt 241 | encoded []byte 242 | }{ 243 | { 244 | decoded: 0, 245 | encoded: []byte{0x00}, 246 | }, 247 | { 248 | decoded: 1, 249 | encoded: []byte{0x01}, 250 | }, 251 | { 252 | decoded: 2, 253 | encoded: []byte{0x02}, 254 | }, 255 | { 256 | decoded: 127, 257 | encoded: []byte{0x7f}, 258 | }, 259 | { 260 | decoded: 128, 261 | encoded: []byte{0x80, 0x01}, 262 | }, 263 | { 264 | decoded: 129, 265 | encoded: []byte{0x81, 0x01}, 266 | }, 267 | { 268 | decoded: 255, 269 | encoded: []byte{0xff, 0x01}, 270 | }, 271 | { 272 | decoded: 256, 273 | encoded: []byte{0x80, 0x02}, 274 | }, 275 | { 276 | decoded: 2097151, 277 | encoded: []byte{0xff, 0xff, 0x7f}, 278 | }, 279 | { 280 | decoded: 2147483647, 281 | encoded: []byte{0xff, 0xff, 0xff, 0xff, 0x07}, 282 | }, 283 | { 284 | decoded: -1, 285 | encoded: []byte{0xff, 0xff, 0xff, 0xff, 0x0f}, 286 | }, 287 | { 288 | decoded: -2147483648, 289 | encoded: []byte{0x80, 0x80, 0x80, 0x80, 0x08}, 290 | }, 291 | } 292 | 293 | for _, tc := range tt { 294 | t.Run(fmt.Sprint(tc.decoded), func(t *testing.T) { 295 | t.Logf("%b", tc.encoded) 296 | reader := bytes.NewReader(tc.encoded) 297 | decodedValue, _ := mc.ReadVarInt(reader) 298 | t.Logf("%b", decodedValue) 299 | if decodedValue != tc.decoded { 300 | t.Errorf("decoding: got %v; want: %v", decodedValue, tc.decoded) 301 | } 302 | }) 303 | } 304 | } 305 | 306 | func TestReadVarInt_ReturnError_WhenLargerThan5(t *testing.T) { 307 | data := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} 308 | _, _, err := mc.ReadPacketSize_Bytes(data) 309 | if !errors.Is(err, mc.ErrVarIntSize) { 310 | t.Fatal("expected an error but didnt got one") 311 | } 312 | 313 | } 314 | -------------------------------------------------------------------------------- /module/conn_creator.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import "net" 4 | 5 | type ConnectionCreator interface { 6 | Conn() func() (net.Conn, error) 7 | } 8 | 9 | type ConnectionCreatorFunc func() (net.Conn, error) 10 | 11 | func (creator ConnectionCreatorFunc) Conn() func() (net.Conn, error) { 12 | return creator 13 | } 14 | 15 | func BasicConnCreator(proxyTo string, dialer net.Dialer) ConnectionCreatorFunc { 16 | return func() (net.Conn, error) { 17 | return dialer.Dial("tcp", proxyTo) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /module/conn_creator_test.go: -------------------------------------------------------------------------------- 1 | package module_test 2 | -------------------------------------------------------------------------------- /module/conn_limiter.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | "time" 7 | 8 | "github.com/realDragonium/Ultraviolet/core" 9 | "github.com/realDragonium/Ultraviolet/mc" 10 | ) 11 | 12 | func FilterIpFromAddr(addr net.Addr) string { 13 | s := addr.String() 14 | parts := strings.Split(s, ":") 15 | return parts[0] 16 | } 17 | 18 | type ConnectionLimiter interface { 19 | Allow(req core.RequestData) bool 20 | } 21 | 22 | func NewAbsConnLimiter(ratelimit int, cooldown time.Duration, limitStatus bool) ConnectionLimiter { 23 | return &absoluteConnlimiter{ 24 | rateLimit: ratelimit, 25 | rateCooldown: cooldown, 26 | limitStatus: limitStatus, 27 | } 28 | } 29 | 30 | type absoluteConnlimiter struct { 31 | rateCounter int 32 | rateStartTime time.Time 33 | rateLimit int 34 | rateCooldown time.Duration 35 | limitStatus bool 36 | } 37 | 38 | func (r *absoluteConnlimiter) Allow(req core.RequestData) bool { 39 | if time.Since(r.rateStartTime) >= r.rateCooldown { 40 | r.rateCounter = 0 41 | r.rateStartTime = time.Now() 42 | } 43 | if !r.limitStatus { 44 | return true 45 | } 46 | if r.rateCounter < r.rateLimit { 47 | r.rateCounter++ 48 | return true 49 | } 50 | return false 51 | } 52 | 53 | type AlwaysAllowConnection struct{} 54 | 55 | func (limiter AlwaysAllowConnection) Allow(req core.RequestData) bool { 56 | return true 57 | } 58 | 59 | func NewBotFilterConnLimiter(ratelimit int, cooldown, clearTime, unverify time.Duration, disconnPk mc.Packet) ConnectionLimiter { 60 | 61 | return &botFilterConnLimiter{ 62 | lastTimeAboveLimit: time.Now(), 63 | unverifyCooldown: unverify, 64 | rateLimit: ratelimit, 65 | rateCooldown: cooldown, 66 | disconnPacket: disconnPk, 67 | listClearTime: clearTime, 68 | 69 | namesList: make(map[string]string), 70 | blackList: make(map[string]time.Time), 71 | } 72 | } 73 | 74 | type botFilterConnLimiter struct { 75 | limiting bool 76 | unverifyCooldown time.Duration 77 | 78 | rateCounter int 79 | rateStartTime time.Time 80 | lastTimeAboveLimit time.Time 81 | rateLimit int 82 | rateCooldown time.Duration 83 | disconnPacket mc.Packet 84 | listClearTime time.Duration 85 | 86 | blackList map[string]time.Time 87 | namesList map[string]string 88 | } 89 | 90 | func (l *botFilterConnLimiter) Allow(req core.RequestData) bool { 91 | if req.Type == mc.Status { 92 | return true 93 | } 94 | 95 | if time.Since(l.rateStartTime) >= l.rateCooldown { 96 | if l.rateCounter > l.rateLimit { 97 | l.lastTimeAboveLimit = l.rateStartTime 98 | } 99 | if l.limiting && time.Since(l.lastTimeAboveLimit) >= l.unverifyCooldown { 100 | l.limiting = false 101 | } 102 | l.rateCounter = 0 103 | l.rateStartTime = time.Now() 104 | } 105 | 106 | l.rateCounter++ 107 | ip := FilterIpFromAddr(req.Addr) 108 | blockTime, ok := l.blackList[ip] 109 | if time.Since(blockTime) >= l.listClearTime { 110 | delete(l.blackList, ip) 111 | } else if ok { 112 | return false 113 | } 114 | 115 | l.limiting = l.limiting || l.rateCounter > l.rateLimit 116 | if l.limiting { 117 | username, ok := l.namesList[ip] 118 | if !ok { 119 | l.namesList[ip] = req.Username 120 | return false 121 | } 122 | if username != req.Username { 123 | l.blackList[ip] = time.Now() 124 | return false 125 | } 126 | } 127 | 128 | return true 129 | } 130 | -------------------------------------------------------------------------------- /module/conn_limiter_test.go: -------------------------------------------------------------------------------- 1 | package module_test 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/realDragonium/Ultraviolet/core" 10 | "github.com/realDragonium/Ultraviolet/mc" 11 | "github.com/realDragonium/Ultraviolet/module" 12 | ) 13 | 14 | func TestAbsoluteConnLimiter_DeniesWhenLimitIsReached(t *testing.T) { 15 | tt := []struct { 16 | limit int 17 | limitStatus bool 18 | cooldown time.Duration 19 | states []mc.HandshakeState 20 | shouldLimit bool 21 | }{ 22 | { 23 | limit: 3, 24 | limitStatus: true, 25 | cooldown: time.Minute, 26 | states: []mc.HandshakeState{mc.Login, mc.Status}, 27 | shouldLimit: true, 28 | }, 29 | { 30 | limit: 3, 31 | limitStatus: false, 32 | cooldown: time.Minute, 33 | states: []mc.HandshakeState{mc.Status}, 34 | shouldLimit: false, 35 | }, 36 | } 37 | 38 | for _, tc := range tt { 39 | name := fmt.Sprintf("limits on: %v, limit status: %v, cooldown: %v", tc.limit, tc.limitStatus, tc.cooldown) 40 | t.Run(name, func(t *testing.T) { 41 | req := core.RequestData{} 42 | connLimiter := module.NewAbsConnLimiter(tc.limit, tc.cooldown, tc.limitStatus) 43 | 44 | for i := 0; i < tc.limit; i++ { 45 | if !connLimiter.Allow(req) { 46 | t.Error("expected ok to be true but its false") 47 | } 48 | } 49 | if connLimiter.Allow(req) == tc.shouldLimit { 50 | t.Error("expected ok to be false but its true") 51 | } 52 | }) 53 | } 54 | 55 | } 56 | 57 | func TestAbsoluteConnLimiter_AllowsNewConnectionsAfterCooldown(t *testing.T) { 58 | limit := 5 59 | cooldown := time.Millisecond 60 | req := core.RequestData{} 61 | connLimiter := module.NewAbsConnLimiter(limit, cooldown, true) 62 | 63 | for i := 0; i < limit+1; i++ { 64 | connLimiter.Allow(req) 65 | } 66 | 67 | time.Sleep(cooldown) 68 | if !connLimiter.Allow(req) { 69 | t.Error("expected ok to be true but its false") 70 | } 71 | 72 | } 73 | 74 | func TestAlwaysAllowConnection(t *testing.T) { 75 | limiter := module.AlwaysAllowConnection{} 76 | req := core.RequestData{} 77 | 78 | if !limiter.Allow(req) { 79 | t.Error("expected ok to be true but its false") 80 | } 81 | 82 | } 83 | 84 | // dont expect to need to generate more than 256 ip addresses 85 | var counter = 0 86 | 87 | func generateIPAddr() net.Addr { 88 | counter++ 89 | return &net.IPAddr{ 90 | IP: net.IPv4(1, 1, 1, byte(counter)), 91 | } 92 | } 93 | 94 | func TestBotFilterConnLimiter(t *testing.T) { 95 | disconMsg := "You have been disconnected" 96 | rateDisconPk := mc.ClientBoundDisconnect{ 97 | Reason: mc.String(disconMsg), 98 | }.Marshal() 99 | listClearTime := time.Minute 100 | normalCooldown := time.Second 101 | normalUnverifyCooldown := 10 * time.Second 102 | 103 | t.Run("testing rate limit border", func(t *testing.T) { 104 | tt := []struct { 105 | rateLimit int 106 | cooldown time.Duration 107 | reqType mc.HandshakeState 108 | allowed bool 109 | }{ 110 | { 111 | rateLimit: 2, 112 | cooldown: time.Second, 113 | reqType: mc.Login, 114 | allowed: true, 115 | }, 116 | { 117 | rateLimit: 0, 118 | cooldown: time.Second, 119 | reqType: mc.Status, 120 | allowed: true, 121 | }, 122 | { 123 | rateLimit: 1, 124 | cooldown: time.Second, 125 | reqType: mc.Login, 126 | allowed: true, 127 | }, 128 | { 129 | rateLimit: 0, 130 | cooldown: time.Second, 131 | reqType: mc.Login, 132 | allowed: false, 133 | }, 134 | } 135 | for _, tc := range tt { 136 | name := fmt.Sprintf("type:%v allowed:%v ratelimit:%d", tc.reqType, tc.allowed, tc.rateLimit) 137 | t.Run(name, func(t *testing.T) { 138 | connLimiter := module.NewBotFilterConnLimiter(tc.rateLimit, tc.cooldown, listClearTime, normalUnverifyCooldown, rateDisconPk) 139 | playerAddr := net.IPAddr{ 140 | IP: net.IPv4(10, 10, 10, 10), 141 | } 142 | playerName := "backend" 143 | req := core.RequestData{ 144 | Type: tc.reqType, 145 | Handshake: mc.ServerBoundHandshake{}, 146 | Addr: &playerAddr, 147 | Username: playerName, 148 | } 149 | 150 | ok := connLimiter.Allow(req) 151 | if ok != tc.allowed { 152 | t.Errorf("expected: %v, got: %v", tc.allowed, ok) 153 | } 154 | }) 155 | } 156 | }) 157 | 158 | t.Run("clears counter when cooldown is over", func(t *testing.T) { 159 | ratelimit := 1 160 | cooldown := time.Millisecond 161 | disconMsg := "You have been disconnected with the server" 162 | disconPk := mc.ClientBoundDisconnect{ 163 | Reason: mc.String(disconMsg), 164 | }.Marshal() 165 | connLimiter := module.NewBotFilterConnLimiter(ratelimit, cooldown, listClearTime, normalUnverifyCooldown, disconPk) 166 | playerAddr := net.IPAddr{ 167 | IP: net.IPv4(10, 10, 10, 10), 168 | } 169 | playerName := "backend" 170 | req := core.RequestData{ 171 | Type: mc.Login, 172 | Handshake: mc.ServerBoundHandshake{}, 173 | Addr: &playerAddr, 174 | Username: playerName, 175 | } 176 | connLimiter.Allow(req) 177 | time.Sleep(cooldown) 178 | 179 | if !connLimiter.Allow(req) { 180 | t.Fatal("expected to be allowed but it was denied") 181 | } 182 | }) 183 | 184 | t.Run("when over ratelimit still limits after cooldown", func(t *testing.T) { 185 | ratelimit := 1 186 | cooldown := time.Millisecond 187 | disconPk := mc.ClientBoundDisconnect{ 188 | Reason: "You have been disconnected with the server", 189 | }.Marshal() 190 | connLimiter := module.NewBotFilterConnLimiter(ratelimit, cooldown, normalUnverifyCooldown, listClearTime, disconPk) 191 | req := core.RequestData{ 192 | Type: mc.Login, 193 | Addr: generateIPAddr(), 194 | Username: "backend", 195 | } 196 | connLimiter.Allow(req) 197 | req.Addr = generateIPAddr() 198 | connLimiter.Allow(req) 199 | time.Sleep(cooldown) 200 | req.Addr = generateIPAddr() 201 | 202 | if connLimiter.Allow(req) { 203 | t.Fatal("expected to be limited but it was allowed") 204 | } 205 | }) 206 | 207 | t.Run("when over ratelimit allow unverified connections again after cooldown", func(t *testing.T) { 208 | unverifyCooldown := time.Millisecond 209 | ratelimit := 1 210 | cooldown := time.Millisecond 211 | disconPk := mc.ClientBoundDisconnect{ 212 | Reason: "You have been disconnected with the server", 213 | }.Marshal() 214 | connLimiter := module.NewBotFilterConnLimiter(ratelimit, cooldown, listClearTime, unverifyCooldown, disconPk) 215 | req := core.RequestData{ 216 | Type: mc.Login, 217 | Addr: generateIPAddr(), 218 | Username: "backend", 219 | } 220 | connLimiter.Allow(req) 221 | req.Addr = generateIPAddr() 222 | connLimiter.Allow(req) 223 | 224 | time.Sleep(2 * cooldown) 225 | 226 | req.Addr = generateIPAddr() 227 | connLimiter.Allow(req) 228 | 229 | time.Sleep(2 * unverifyCooldown) 230 | req.Addr = generateIPAddr() 231 | 232 | if !connLimiter.Allow(req) { 233 | t.Fatal("expected to be allowed but it was denied") 234 | } 235 | }) 236 | 237 | t.Run("over rate limit allows second login request", func(t *testing.T) { 238 | ratelimit := 0 239 | connLimiter := module.NewBotFilterConnLimiter(ratelimit, normalCooldown, listClearTime, normalUnverifyCooldown, rateDisconPk) 240 | playerAddr := net.IPAddr{ 241 | IP: net.IPv4(10, 10, 10, 10), 242 | } 243 | playerName := "backend" 244 | req := core.RequestData{ 245 | Type: mc.Login, 246 | Handshake: mc.ServerBoundHandshake{}, 247 | Addr: &playerAddr, 248 | Username: playerName, 249 | } 250 | connLimiter.Allow(req) 251 | 252 | if !connLimiter.Allow(req) { 253 | t.Fatal("expected to be allowed but it was denied") 254 | } 255 | }) 256 | 257 | t.Run("second request with different name gets denied", func(t *testing.T) { 258 | ratelimit := 0 259 | connLimiter := module.NewBotFilterConnLimiter(ratelimit, normalCooldown, listClearTime, normalUnverifyCooldown, rateDisconPk) 260 | playerAddr := net.IPAddr{ 261 | IP: net.IPv4(10, 10, 10, 10), 262 | } 263 | playerName1 := "backend" 264 | playerName2 := "uv" 265 | req := core.RequestData{ 266 | Type: mc.Login, 267 | Handshake: mc.ServerBoundHandshake{}, 268 | Addr: &playerAddr, 269 | Username: playerName1, 270 | } 271 | req2 := req 272 | req2.Username = playerName2 273 | connLimiter.Allow(req) 274 | 275 | if connLimiter.Allow(req2) { 276 | t.Fatal("expected to be denied but it was allowed") 277 | } 278 | }) 279 | 280 | t.Run("when blocked extra request gets denied", func(t *testing.T) { 281 | ratelimit := 0 282 | connLimiter := module.NewBotFilterConnLimiter(ratelimit, normalCooldown, listClearTime, normalUnverifyCooldown, rateDisconPk) 283 | playerAddr := net.IPAddr{ 284 | IP: net.IPv4(10, 10, 10, 10), 285 | } 286 | playerName1 := "backend" 287 | playerName2 := "uv" 288 | req := core.RequestData{ 289 | Type: mc.Login, 290 | Handshake: mc.ServerBoundHandshake{}, 291 | Addr: &playerAddr, 292 | Username: playerName1, 293 | } 294 | req2 := req 295 | req2.Username = playerName2 296 | connLimiter.Allow(req) 297 | connLimiter.Allow(req2) 298 | 299 | if connLimiter.Allow(req) { 300 | t.Fatal("expected to be denied but it was allowed") 301 | } 302 | }) 303 | 304 | t.Run("when blocked after normal cooldown still blocked", func(t *testing.T) { 305 | ratelimit := 1 306 | cooldown := time.Millisecond 307 | connLimiter := module.NewBotFilterConnLimiter(ratelimit, cooldown, listClearTime, normalUnverifyCooldown, rateDisconPk) 308 | playerAddr := net.IPAddr{ 309 | IP: net.IPv4(10, 10, 10, 10), 310 | } 311 | playerName1 := "backend" 312 | playerName2 := "uv" 313 | req := core.RequestData{ 314 | Type: mc.Login, 315 | Handshake: mc.ServerBoundHandshake{}, 316 | Addr: &playerAddr, 317 | Username: playerName1, 318 | } 319 | req2 := req 320 | req2.Username = playerName2 321 | connLimiter.Allow(req) 322 | connLimiter.Allow(req) 323 | connLimiter.Allow(req2) 324 | time.Sleep(cooldown * 2) 325 | 326 | if connLimiter.Allow(req) { 327 | t.Fatal("expected to be denied but it was allowed") 328 | } 329 | }) 330 | 331 | t.Run("is NOT blocked anymore after listClear & unverify cooldown", func(t *testing.T) { 332 | unverifyCooldown := 10 * time.Microsecond 333 | ratelimit := 1 334 | cooldown := time.Millisecond 335 | listClearCooldown := 10 * time.Millisecond 336 | connLimiter := module.NewBotFilterConnLimiter(ratelimit, cooldown, listClearTime, unverifyCooldown, rateDisconPk) 337 | playerName1 := "backend" 338 | playerName2 := "uv" 339 | req := core.RequestData{ 340 | Type: mc.Login, 341 | Handshake: mc.ServerBoundHandshake{}, 342 | Addr: generateIPAddr(), 343 | Username: playerName1, 344 | } 345 | req2 := req 346 | req2.Username = playerName2 347 | connLimiter.Allow(req) 348 | connLimiter.Allow(req2) 349 | time.Sleep(2 * listClearCooldown) 350 | t.Log("last check ----------------") 351 | 352 | if !connLimiter.Allow(req) { 353 | t.Fatal("expected to be allowed but it was denied") 354 | } 355 | }) 356 | } 357 | -------------------------------------------------------------------------------- /module/handshake_modifier.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | 6 | "github.com/realDragonium/Ultraviolet/mc" 7 | ) 8 | 9 | type HandshakeModifier interface { 10 | Modify(hs *mc.ServerBoundHandshake, addr string) 11 | } 12 | 13 | func NewRealIP2_4() realIPv2_4 { 14 | return realIPv2_4{} 15 | } 16 | 17 | type realIPv2_4 struct{} 18 | 19 | func (rip realIPv2_4) Modify(hs *mc.ServerBoundHandshake, addr string) { 20 | hs.UpgradeToOldRealIP(addr) 21 | } 22 | 23 | func NewRealIP2_5(key *ecdsa.PrivateKey) realIPv2_5 { 24 | return realIPv2_5{ 25 | realIPKey: key, 26 | } 27 | } 28 | 29 | type realIPv2_5 struct { 30 | realIPKey *ecdsa.PrivateKey 31 | } 32 | 33 | func (rip realIPv2_5) Modify(hs *mc.ServerBoundHandshake, addr string) { 34 | hs.UpgradeToNewRealIP(addr, rip.realIPKey) 35 | } 36 | -------------------------------------------------------------------------------- /module/handshake_modifier_test.go: -------------------------------------------------------------------------------- 1 | package module_test 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/realDragonium/Ultraviolet/mc" 11 | "github.com/realDragonium/Ultraviolet/module" 12 | ) 13 | 14 | func TestRateLimter_RealIPv2_4(t *testing.T) { 15 | handshakeModifier := module.NewRealIP2_4() 16 | handshake := mc.ServerBoundHandshake{ 17 | ProtocolVersion: 755, 18 | ServerAddress: "ultraviolet", 19 | ServerPort: 25565, 20 | NextState: 2, 21 | } 22 | 23 | handshakeModifier.Modify(&handshake, "192.32.27.85:57493") 24 | if !handshake.IsRealIPAddress() { 25 | t.Fatal("Should have changed to RealIP format") 26 | } 27 | parts := strings.Split(handshake.ServerAddress, mc.RealIPSeparator) 28 | if len(parts) != 3 { 29 | t.Errorf("Wrong RealIP format?!? %v", parts) 30 | } 31 | } 32 | 33 | func TestRateLimter_RealIPv2_5(t *testing.T) { 34 | privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 35 | if err != nil { 36 | t.Fatalf("got error: %v", err) 37 | } 38 | handshakeModifier := module.NewRealIP2_5(privKey) 39 | handshake := mc.ServerBoundHandshake{ 40 | ProtocolVersion: 755, 41 | ServerAddress: "ultraviolet", 42 | ServerPort: 25565, 43 | NextState: 2, 44 | } 45 | 46 | handshakeModifier.Modify(&handshake, "192.32.27.85:57493") 47 | if !handshake.IsRealIPAddress() { 48 | t.Fatal("Should have changed to RealIP format") 49 | } 50 | parts := strings.Split(handshake.ServerAddress, mc.RealIPSeparator) 51 | if len(parts) != 4 { 52 | t.Errorf("wrong RealIP format? %v", parts) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /module/state_agent.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/realDragonium/Ultraviolet/core" 7 | ) 8 | 9 | type StateAgent interface { 10 | State() core.ServerState 11 | } 12 | 13 | func NewMcServerState(cooldown time.Duration, connCreator ConnectionCreator) StateAgent { 14 | return &McServerState{ 15 | state: core.Unknown, 16 | cooldown: cooldown, 17 | connCreator: connCreator, 18 | startTime: time.Time{}, 19 | } 20 | } 21 | 22 | type McServerState struct { 23 | state core.ServerState 24 | cooldown time.Duration 25 | startTime time.Time 26 | connCreator ConnectionCreator 27 | } 28 | 29 | func (server *McServerState) State() core.ServerState { 30 | if time.Since(server.startTime) <= server.cooldown { 31 | return server.state 32 | } 33 | server.startTime = time.Now() 34 | connFunc := server.connCreator.Conn() 35 | conn, err := connFunc() 36 | if err != nil { 37 | server.state = core.Offline 38 | } else { 39 | server.state = core.Online 40 | conn.Close() 41 | } 42 | return server.state 43 | } 44 | 45 | type AlwaysOnlineState struct{} 46 | 47 | func (agent AlwaysOnlineState) State() core.ServerState { 48 | return core.Online 49 | } 50 | 51 | type AlwaysOfflineState struct{} 52 | 53 | func (agent AlwaysOfflineState) State() core.ServerState { 54 | return core.Offline 55 | } 56 | -------------------------------------------------------------------------------- /module/state_agent_test.go: -------------------------------------------------------------------------------- 1 | package module_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "testing" 8 | "time" 9 | 10 | "github.com/realDragonium/Ultraviolet/core" 11 | "github.com/realDragonium/Ultraviolet/module" 12 | ) 13 | 14 | var ( 15 | ErrEmptyConnCreator = errors.New("this is a test conn creator which doesnt provide connections") 16 | defaultChTimeout = 25 * time.Millisecond 17 | ) 18 | 19 | func TestAlwaysOnlineState(t *testing.T) { 20 | stateAgent := module.AlwaysOnlineState{} 21 | 22 | if stateAgent.State() != core.Online { 23 | t.Errorf("expected to be online but got %v instead", stateAgent.State()) 24 | } 25 | } 26 | 27 | func TestAlwaysOfflineState(t *testing.T) { 28 | stateAgent := module.AlwaysOfflineState{} 29 | 30 | if stateAgent.State() != core.Offline { 31 | t.Errorf("expected to be offline but got %v instead", stateAgent.State()) 32 | } 33 | } 34 | 35 | type stateConnCreator struct { 36 | callAmount int 37 | returnError bool 38 | } 39 | 40 | func (creator *stateConnCreator) Conn() func() (net.Conn, error) { 41 | creator.callAmount++ 42 | if creator.returnError { 43 | return func() (net.Conn, error) { 44 | return nil, ErrEmptyConnCreator 45 | } 46 | } 47 | return func() (net.Conn, error) { 48 | return &net.TCPConn{}, nil 49 | } 50 | } 51 | 52 | func TestMcServerState(t *testing.T) { 53 | tt := []struct { 54 | returnError bool 55 | expectedState core.ServerState 56 | }{ 57 | { 58 | expectedState: core.Offline, 59 | returnError: true, 60 | }, 61 | { 62 | expectedState: core.Online, 63 | returnError: false, 64 | }, 65 | } 66 | t.Run("single run state", func(t *testing.T) { 67 | for _, tc := range tt { 68 | name := fmt.Sprintf("returnError:%v - expectedState:%v", tc.returnError, tc.expectedState) 69 | t.Run(name, func(t *testing.T) { 70 | cooldown := time.Minute 71 | connCreator := stateConnCreator{ 72 | returnError: tc.returnError, 73 | } 74 | stateAgent := module.NewMcServerState(cooldown, &connCreator) 75 | state := stateAgent.State() 76 | if state != tc.expectedState { 77 | t.Errorf("expected to be %v but got %v instead", tc.expectedState, state) 78 | } 79 | if connCreator.callAmount != 1 { 80 | t.Errorf("expected connCreator to be called %v times but was called %v time", 1, connCreator.callAmount) 81 | } 82 | }) 83 | } 84 | }) 85 | 86 | t.Run("doesnt call again while in cooldown", func(t *testing.T) { 87 | for _, tc := range tt { 88 | name := fmt.Sprintf("returnError:%v - expectedState:%v", tc.returnError, tc.expectedState) 89 | t.Run(name, func(t *testing.T) { 90 | cooldown := time.Minute 91 | connCreator := stateConnCreator{ 92 | returnError: tc.returnError, 93 | } 94 | stateAgent := module.NewMcServerState(cooldown, &connCreator) 95 | stateAgent.State() 96 | state := stateAgent.State() 97 | if state != tc.expectedState { 98 | t.Errorf("expected to be %v but got %v instead", tc.expectedState, state) 99 | } 100 | if connCreator.callAmount != 1 { 101 | t.Errorf("expected connCreator to be called %v times but was called %v time", 1, connCreator.callAmount) 102 | } 103 | }) 104 | } 105 | }) 106 | 107 | t.Run("does call again after cooldown", func(t *testing.T) { 108 | for _, tc := range tt { 109 | name := fmt.Sprintf("returnError:%v - expectedState:%v", tc.returnError, tc.expectedState) 110 | t.Run(name, func(t *testing.T) { 111 | cooldown := time.Millisecond 112 | connCreator := stateConnCreator{ 113 | returnError: tc.returnError, 114 | } 115 | stateAgent := module.NewMcServerState(cooldown, &connCreator) 116 | stateAgent.State() 117 | time.Sleep(cooldown) 118 | state := stateAgent.State() 119 | if state != tc.expectedState { 120 | t.Errorf("expected to be %v but got %v instead", tc.expectedState, state) 121 | } 122 | if connCreator.callAmount != 2 { 123 | t.Errorf("expected connCreator to be called %v times but was called %v time", 2, connCreator.callAmount) 124 | } 125 | }) 126 | } 127 | }) 128 | } 129 | -------------------------------------------------------------------------------- /module/status_cache.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/realDragonium/Ultraviolet/core" 8 | "github.com/realDragonium/Ultraviolet/mc" 9 | ) 10 | 11 | type StatusCache interface { 12 | Status() (mc.Packet, error) 13 | } 14 | 15 | func NewStatusCache(protocol int, cooldown time.Duration, connCreator ConnectionCreator) StatusCache { 16 | handshake := mc.ServerBoundHandshake{ 17 | ProtocolVersion: protocol, 18 | ServerAddress: "Ultraviolet", 19 | ServerPort: 25565, 20 | NextState: 1, 21 | } 22 | 23 | return &statusCache{ 24 | connCreator: connCreator, 25 | cooldown: cooldown, 26 | handshake: handshake, 27 | } 28 | } 29 | 30 | type statusCache struct { 31 | connCreator ConnectionCreator 32 | 33 | status mc.Packet 34 | cooldown time.Duration 35 | cacheTime time.Time 36 | handshake mc.ServerBoundHandshake 37 | } 38 | 39 | func (cache *statusCache) Status() (mc.Packet, error) { 40 | if time.Since(cache.cacheTime) < cache.cooldown { 41 | return cache.status, nil 42 | } 43 | answer, err := cache.newStatus() 44 | if err != nil && !errors.Is(err, core.ErrStatusPing) { 45 | return cache.status, err 46 | } 47 | cache.cacheTime = time.Now() 48 | cache.status = answer 49 | return cache.status, nil 50 | } 51 | 52 | func (cache *statusCache) newStatus() (pk mc.Packet, err error) { 53 | connFunc := cache.connCreator.Conn() 54 | conn, err := connFunc() 55 | if err != nil { 56 | return 57 | } 58 | 59 | mcConn := mc.NewMcConn(conn) 60 | if err = mcConn.WriteMcPacket(cache.handshake); err != nil { 61 | return 62 | } 63 | if err = mcConn.WritePacket(mc.ServerBoundRequest{}.Marshal()); err != nil { 64 | return 65 | } 66 | 67 | pk, err = mcConn.ReadPacket() 68 | if err != nil { 69 | return 70 | } 71 | 72 | conn.Close() 73 | return 74 | } 75 | -------------------------------------------------------------------------------- /module/status_cache_test.go: -------------------------------------------------------------------------------- 1 | package module_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "testing" 8 | "time" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | "github.com/realDragonium/Ultraviolet/mc" 12 | "github.com/realDragonium/Ultraviolet/module" 13 | ) 14 | 15 | type statusCacheConnCreator struct { 16 | conn net.Conn 17 | err error 18 | } 19 | 20 | func (creator statusCacheConnCreator) Conn() func() (net.Conn, error) { 21 | return func() (net.Conn, error) { 22 | return creator.conn, creator.err 23 | } 24 | } 25 | 26 | type statusCacheConnCreatorMultipleCalls struct { 27 | connCh <-chan net.Conn 28 | } 29 | 30 | func (creator statusCacheConnCreatorMultipleCalls) Conn() func() (net.Conn, error) { 31 | conn := <-creator.connCh 32 | return func() (net.Conn, error) { 33 | return conn, nil 34 | } 35 | } 36 | 37 | func statusCall_TestError(t *testing.T, cache *module.StatusCache, errCh chan error) mc.Packet { 38 | t.Helper() 39 | answerCh := make(chan mc.Packet) 40 | go func() { 41 | answer, err := (*cache).Status() 42 | if err != nil { 43 | errCh <- err 44 | return 45 | } 46 | answerCh <- answer 47 | }() 48 | 49 | select { 50 | case answer := <-answerCh: 51 | t.Log("worker has successfully responded") 52 | return answer 53 | case err := <-errCh: 54 | t.Fatalf("didnt expect an error but got: %v", err) 55 | } 56 | return mc.Packet{} 57 | } 58 | 59 | type serverSimulator struct { 60 | callAmount int 61 | closeConnByStep int 62 | } 63 | 64 | func (simulator *serverSimulator) simulateServerStatus(conn net.Conn, statusPacket mc.Packet) error { 65 | simulator.callAmount++ 66 | mcConn := mc.NewMcConn(conn) 67 | if simulator.closeConnByStep == 1 { 68 | return conn.Close() 69 | } 70 | _, err := mcConn.ReadPacket() 71 | if err != nil { 72 | return err 73 | } 74 | if simulator.closeConnByStep == 2 { 75 | return conn.Close() 76 | } 77 | _, err = mcConn.ReadPacket() 78 | if err != nil { 79 | return err 80 | } 81 | if simulator.closeConnByStep == 3 { 82 | return conn.Close() 83 | } 84 | err = mcConn.WritePacket(statusPacket) 85 | if err != nil { 86 | return err 87 | } 88 | if simulator.closeConnByStep == 4 { 89 | return conn.Close() 90 | } 91 | pingPk, err := mcConn.ReadPacket() 92 | if err != nil { 93 | return err 94 | } 95 | time.Sleep(defaultChTimeout / 10) // '/ 10' part just so its shorter than the time.After later 96 | if simulator.closeConnByStep == 5 { 97 | return conn.Close() 98 | } 99 | err = mcConn.WritePacket(pingPk) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func TestStatusCache(t *testing.T) { 108 | protocolVersion := 755 109 | cooldown := time.Minute 110 | statusPacket := mc.SimpleStatus{ 111 | Name: "backend", 112 | Protocol: protocolVersion, 113 | Description: "some random motd text", 114 | }.Marshal() 115 | 116 | t.Run("normal flow", func(t *testing.T) { 117 | errCh := make(chan error) 118 | answerCh := make(chan mc.Packet) 119 | c1, c2 := net.Pipe() 120 | connCreator := statusCacheConnCreator{conn: c1} 121 | statusCache := module.NewStatusCache(protocolVersion, cooldown, connCreator) 122 | simulator := serverSimulator{} 123 | go func() { 124 | err := simulator.simulateServerStatus(c2, statusPacket) 125 | if err != nil { 126 | errCh <- err 127 | } 128 | }() 129 | go func() { 130 | answer, err := statusCache.Status() 131 | if err != nil { 132 | errCh <- err 133 | } 134 | answerCh <- answer 135 | }() 136 | 137 | var answer mc.Packet 138 | 139 | select { 140 | case answer = <-answerCh: 141 | t.Log("worker has successfully responded") 142 | case err := <-errCh: 143 | t.Fatalf("didnt expect an error but got: %v", err) 144 | case <-time.After(defaultChTimeout): 145 | t.Fatal("timed out") 146 | } 147 | 148 | if !cmp.Equal(statusPacket, answer) { 149 | t.Error("received different packet than we expected!") 150 | t.Logf("expected: %#v", statusPacket) 151 | t.Logf("received: %#v", answer) 152 | } 153 | if simulator.callAmount != 1 { 154 | t.Errorf("expected backend to be called 1 time but got called %v time(s)", simulator.callAmount) 155 | } 156 | }) 157 | 158 | t.Run("doesnt call again while in cooldown", func(t *testing.T) { 159 | errCh := make(chan error) 160 | connCh := make(chan net.Conn, 1) 161 | connCreator := &statusCacheConnCreatorMultipleCalls{connCh: connCh} 162 | statusCache := module.NewStatusCache(protocolVersion, cooldown, connCreator) 163 | simulator := serverSimulator{} 164 | 165 | c1, c2 := net.Pipe() 166 | connCh <- c1 167 | go simulator.simulateServerStatus(c2, statusPacket) 168 | statusCall_TestError(t, &statusCache, errCh) 169 | 170 | // This will timeout if its going to call a second time 171 | statusCall_TestError(t, &statusCache, errCh) 172 | if simulator.callAmount != 1 { 173 | t.Errorf("expected backend to be called 1 time but got called %v time(s)", simulator.callAmount) 174 | } 175 | }) 176 | 177 | t.Run("does call again after cooldown", func(t *testing.T) { 178 | cooldown = time.Microsecond 179 | errCh := make(chan error) 180 | connCh := make(chan net.Conn, 1) 181 | connCreator := &statusCacheConnCreatorMultipleCalls{connCh: connCh} 182 | statusCache := module.NewStatusCache(protocolVersion, cooldown, connCreator) 183 | simulator := serverSimulator{} 184 | 185 | c1, c2 := net.Pipe() 186 | connCh <- c1 187 | go simulator.simulateServerStatus(c2, statusPacket) 188 | statusCall_TestError(t, &statusCache, errCh) 189 | time.Sleep(cooldown) 190 | c1, c2 = net.Pipe() 191 | connCh <- c1 192 | go simulator.simulateServerStatus(c2, statusPacket) 193 | statusCall_TestError(t, &statusCache, errCh) 194 | if simulator.callAmount != 2 { 195 | t.Errorf("expected backend to be called 2 time but got called %v time(s)", simulator.callAmount) 196 | } 197 | }) 198 | 199 | t.Run("returns with error when connCreator returns error ", func(t *testing.T) { 200 | t.Run("with conn being nil", func(t *testing.T) { 201 | usedError := errors.New("cant create connection") 202 | connCreator := statusCacheConnCreator{err: usedError, conn: nil} 203 | statusCache := module.NewStatusCache(protocolVersion, cooldown, connCreator) 204 | _, err := statusCache.Status() 205 | if !errors.Is(err, usedError) { 206 | t.Errorf("expected an error but something else: %v", err) 207 | } 208 | }) 209 | t.Run("with conn being an connection", func(t *testing.T) { 210 | usedError := errors.New("cant create connection") 211 | connCreator := statusCacheConnCreator{err: usedError, conn: &net.TCPConn{}} 212 | statusCache := module.NewStatusCache(protocolVersion, cooldown, connCreator) 213 | _, err := statusCache.Status() 214 | if !errors.Is(err, usedError) { 215 | t.Errorf("expected an error but something else: %v", err) 216 | } 217 | }) 218 | }) 219 | 220 | t.Run("test closing connection early", func(t *testing.T) { 221 | tt := []struct { 222 | matchStatus bool 223 | shouldReturnError bool 224 | closeConnByStep int 225 | }{ 226 | { 227 | matchStatus: false, 228 | shouldReturnError: true, 229 | closeConnByStep: 1, 230 | }, 231 | { 232 | matchStatus: false, 233 | shouldReturnError: true, 234 | closeConnByStep: 2, 235 | }, 236 | { 237 | matchStatus: false, 238 | shouldReturnError: true, 239 | closeConnByStep: 3, 240 | }, 241 | { 242 | matchStatus: true, 243 | shouldReturnError: false, 244 | closeConnByStep: 4, 245 | }, 246 | { 247 | matchStatus: true, 248 | shouldReturnError: false, 249 | closeConnByStep: 5, 250 | }, 251 | } 252 | 253 | for _, tc := range tt { 254 | name := fmt.Sprintf("closeConnBy:%v", tc.closeConnByStep) 255 | t.Run(name, func(t *testing.T) { 256 | errCh := make(chan error) 257 | answerCh := make(chan mc.Packet) 258 | 259 | c1, c2 := net.Pipe() 260 | connCreator := statusCacheConnCreator{conn: c1} 261 | statusCache := module.NewStatusCache(protocolVersion, cooldown, connCreator) 262 | simulator := serverSimulator{ 263 | closeConnByStep: tc.closeConnByStep, 264 | } 265 | go func() { 266 | err := simulator.simulateServerStatus(c2, statusPacket) 267 | if err != nil { 268 | errCh <- err 269 | } 270 | }() 271 | closeCh := make(chan struct{}) 272 | go func() { 273 | answer, err := statusCache.Status() 274 | if err != nil { 275 | errCh <- err 276 | } 277 | select { 278 | case answerCh <- answer: 279 | case <-closeCh: 280 | } 281 | 282 | }() 283 | 284 | var answer mc.Packet 285 | var err error 286 | select { 287 | case answer = <-answerCh: 288 | t.Log("worker has successfully responded") 289 | case err = <-errCh: 290 | closeCh <- struct{}{} 291 | if !tc.shouldReturnError { 292 | t.Fatalf("didnt expect an error but got: %v", err) 293 | } 294 | case <-time.After(defaultChTimeout): 295 | closeCh <- struct{}{} 296 | t.Fatal("timed out") 297 | } 298 | 299 | if err == nil && tc.shouldReturnError { 300 | t.Fatal("expected an error but got nothing") 301 | } 302 | 303 | if tc.matchStatus && !cmp.Equal(statusPacket, answer) { 304 | t.Error("received different packet than we expected!") 305 | t.Logf("expected: %v", statusPacket) 306 | t.Logf("received: %v", answer) 307 | } 308 | 309 | }) 310 | } 311 | }) 312 | } 313 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package ultraviolet 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net" 7 | 8 | "github.com/realDragonium/Ultraviolet/config" 9 | "github.com/realDragonium/Ultraviolet/core" 10 | ) 11 | 12 | var ( 13 | maxConnParallelProcessing = make(chan struct{}, 100_000) 14 | ) 15 | 16 | func NewProxy(uvReader config.UVConfigReader, l net.Listener, cfgReader config.ServerConfigReader) core.Proxy { 17 | return &SpeedyProxy{ 18 | uvReader: uvReader, 19 | listener: l, 20 | cfgReader: cfgReader, 21 | } 22 | } 23 | 24 | type SpeedyProxy struct { 25 | uvReader config.UVConfigReader 26 | listener net.Listener 27 | cfgReader config.ServerConfigReader 28 | serverCatalog core.ServerCatalog 29 | } 30 | 31 | func (p *SpeedyProxy) Start() error { 32 | p.ReloadServerCatalog() 33 | 34 | for { 35 | conn, err := p.listener.Accept() 36 | if errors.Is(err, net.ErrClosed) { 37 | log.Printf("net.Listener was closed, stopping with accepting calls") 38 | break 39 | } else if err != nil { 40 | log.Printf("got error: %v", err) 41 | continue 42 | } 43 | 44 | claimParallelRun() 45 | go func(conn net.Conn, serverCatalog core.ServerCatalog) { 46 | defer unclaimParallelRun() 47 | FullRun(conn, serverCatalog) 48 | }(conn, p.serverCatalog) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func claimParallelRun() { 55 | maxConnParallelProcessing <- struct{}{} 56 | } 57 | 58 | func unclaimParallelRun() { 59 | <-maxConnParallelProcessing 60 | } 61 | 62 | func (p *SpeedyProxy) ReloadServerCatalog() error { 63 | cfg, err := p.uvReader() 64 | if err != nil { 65 | return err 66 | } 67 | 68 | servers := make(map[string]core.Server) 69 | 70 | newCfgs, err := p.cfgReader() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | for _, newCfg := range newCfgs { 76 | apiCfg, err := config.ServerToAPIConfig(newCfg) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | server := NewAPIServer(apiCfg) 82 | for _, domain := range newCfg.Domains { 83 | servers[domain] = server 84 | } 85 | } 86 | 87 | p.serverCatalog = core.NewServerCatalog(servers, cfg.DefaultStatusPk(), cfg.VerifyConnectionPk()) 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /proxy_test.go: -------------------------------------------------------------------------------- 1 | package ultraviolet_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/google/go-cmp/cmp" 14 | "github.com/pires/go-proxyproto" 15 | ultraviolet "github.com/realDragonium/Ultraviolet" 16 | "github.com/realDragonium/Ultraviolet/config" 17 | "github.com/realDragonium/Ultraviolet/mc" 18 | ) 19 | 20 | var ( 21 | port *int16 22 | portLock sync.Mutex = sync.Mutex{} 23 | ) 24 | 25 | func newTestLogger(t *testing.T) io.Writer { 26 | return &testLogger{ 27 | t: t, 28 | } 29 | } 30 | 31 | type testLogger struct { 32 | t *testing.T 33 | } 34 | 35 | func (logger *testLogger) Write(bb []byte) (int, error) { 36 | logger.t.Logf("%s", bb) 37 | return 0, nil 38 | } 39 | 40 | // To make sure every test gets its own unique port 41 | func testAddr() string { 42 | portLock.Lock() 43 | defer portLock.Unlock() 44 | if port == nil { 45 | port = new(int16) 46 | *port = 26000 47 | } 48 | addr := fmt.Sprintf("127.0.0.1:%d", *port) 49 | *port++ 50 | return addr 51 | } 52 | 53 | // Returns address of the server running 54 | func StartProxy(cfg config.UltravioletConfig) (string, error) { 55 | serverAddr := testAddr() 56 | cfg.ListenTo = serverAddr 57 | uvReader := testUVReader{ 58 | cfg: cfg, 59 | } 60 | serverCfgReader := testServerCfgReader{} 61 | listener, err := net.Listen("tcp", serverAddr) 62 | if err != nil { 63 | return serverAddr, err 64 | } 65 | proxy := ultraviolet.NewProxy(uvReader.Read, listener, serverCfgReader.Read) 66 | go proxy.Start() 67 | return serverAddr, nil 68 | } 69 | 70 | func TestProxyProtocol(t *testing.T) { 71 | t.SkipNow() 72 | tt := []struct { 73 | acceptProxyProtocol bool 74 | sendProxyProtocol bool 75 | shouldClose bool 76 | }{ 77 | { 78 | acceptProxyProtocol: true, 79 | sendProxyProtocol: true, 80 | shouldClose: false, 81 | }, 82 | { 83 | acceptProxyProtocol: true, 84 | sendProxyProtocol: false, 85 | shouldClose: true, 86 | }, 87 | { 88 | acceptProxyProtocol: false, 89 | sendProxyProtocol: true, 90 | shouldClose: true, 91 | }, 92 | } 93 | 94 | for _, tc := range tt { 95 | name := fmt.Sprintf("accept:%v - send:%v", tc.acceptProxyProtocol, tc.sendProxyProtocol) 96 | t.Run(name, func(t *testing.T) { 97 | serverDomain := "Ultraviolet" 98 | defaultStatus := mc.SimpleStatus{ 99 | Name: "uv", 100 | Protocol: 710, 101 | Description: "something", 102 | } 103 | cfg := config.UltravioletConfig{ 104 | NumberOfWorkers: 1, 105 | NumberOfListeners: 1, 106 | AcceptProxyProtocol: tc.acceptProxyProtocol, 107 | IODeadline: time.Millisecond, 108 | DefaultStatus: defaultStatus, 109 | LogOutput: newTestLogger(t), 110 | } 111 | serverAddr, err := StartProxy(cfg) 112 | if err != nil { 113 | t.Fatalf("received error: %v", err) 114 | } 115 | conn, err := net.Dial("tcp", serverAddr) 116 | if err != nil { 117 | t.Fatalf("received error: %v", err) 118 | } 119 | 120 | if tc.sendProxyProtocol { 121 | header := &proxyproto.Header{ 122 | Version: 1, 123 | Command: proxyproto.PROXY, 124 | TransportProtocol: proxyproto.TCPv4, 125 | SourceAddr: &net.TCPAddr{ 126 | IP: net.ParseIP("10.1.1.1"), 127 | Port: 1000, 128 | }, 129 | DestinationAddr: &net.TCPAddr{ 130 | IP: net.ParseIP("20.2.2.2"), 131 | Port: 2000, 132 | }, 133 | } 134 | _, err = header.WriteTo(conn) 135 | if err != nil { 136 | t.Fatalf("received error: %v", err) 137 | } 138 | } 139 | 140 | serverConn := mc.NewMcConn(conn) 141 | handshake := mc.ServerBoundHandshake{ 142 | ServerAddress: serverDomain, 143 | NextState: mc.StatusState, 144 | }.Marshal() 145 | 146 | err = serverConn.WritePacket(handshake) 147 | if err != nil { 148 | t.Fatalf("received error: %v", err) 149 | } 150 | err = serverConn.WritePacket(mc.Packet{ID: mc.ServerBoundRequestPacketID}) 151 | if err != nil { 152 | t.Fatalf("received error: %v", err) 153 | } 154 | pk, err := serverConn.ReadPacket() 155 | if tc.shouldClose { 156 | if errors.Is(err, io.EOF) { 157 | return 158 | } 159 | t.Fatalf("expected an EOF error but got: %v", err) 160 | } 161 | if err != nil { 162 | t.Fatalf("didnt expect an error but got: %v", err) 163 | } 164 | 165 | expectedStatus := defaultStatus 166 | expectedStatusPacket := expectedStatus.Marshal() 167 | if !cmp.Equal(expectedStatusPacket, pk) { 168 | expected, _ := mc.UnmarshalClientBoundResponse(expectedStatusPacket) 169 | received, _ := mc.UnmarshalClientBoundResponse(pk) 170 | t.Errorf("expcted: %v \ngot: %v", expected, received) 171 | } 172 | 173 | }) 174 | } 175 | } 176 | 177 | type testServerCfgReader struct { 178 | } 179 | 180 | func (reader *testServerCfgReader) Read() ([]config.ServerConfig, error) { 181 | return nil, nil 182 | } 183 | 184 | type testUVReader struct { 185 | called bool 186 | cfg config.UltravioletConfig 187 | } 188 | 189 | func (reader *testUVReader) Read() (config.UltravioletConfig, error) { 190 | reader.called = true 191 | return reader.cfg, nil 192 | } 193 | 194 | var defaultStatus = mc.SimpleStatus{ 195 | Name: "uv", 196 | Protocol: 1, 197 | Description: "something", 198 | } 199 | 200 | var defaultStatusPk = defaultStatus.Marshal() 201 | 202 | func TestFlowProxy(t *testing.T) { 203 | tt := []struct { 204 | name string 205 | handshake mc.Packet 206 | secondPk mc.Packet 207 | expectedPk mc.Packet 208 | servers []config.ServerConfig 209 | }{ 210 | { 211 | name: "no server found - default status", 212 | handshake: statusHsPk, 213 | secondPk: statusSecondPk, 214 | expectedPk: defaultStatusPk, 215 | }, 216 | { 217 | name: "server found - status", 218 | handshake: statusHsPk, 219 | secondPk: statusSecondPk, 220 | expectedPk: defaultStatusPk, 221 | servers: []config.ServerConfig{ 222 | { 223 | Domains: []string{"Ultraviolet"}, 224 | 225 | }, 226 | }, 227 | }, 228 | } 229 | 230 | for _, tc := range tt { 231 | t.Run(tc.name, func(t *testing.T) { 232 | cfg := config.UltravioletConfig{ 233 | IODeadline: time.Millisecond, 234 | DefaultStatus: defaultStatus, 235 | LogOutput: newTestLogger(t), 236 | } 237 | 238 | serverAddr, err := StartProxy(cfg) 239 | if err != nil { 240 | t.Fatalf("received error: %v", err) 241 | } 242 | log.Println(serverAddr) 243 | conn, err := net.Dial("tcp", serverAddr) 244 | if err != nil { 245 | t.Fatalf("received error: %v", err) 246 | } 247 | 248 | mcConn := mc.NewMcConn(conn) 249 | err = mcConn.WritePacket(tc.handshake) 250 | if err != nil { 251 | t.Fatalf("received error: %v", err) 252 | } 253 | 254 | err = mcConn.WritePacket(tc.secondPk) 255 | if err != nil { 256 | t.Fatalf("received error: %v", err) 257 | } 258 | 259 | pk, err := mcConn.ReadPacket() 260 | if err != nil { 261 | t.Fatalf("didnt expect an error but got: %v", err) 262 | } 263 | 264 | if !cmp.Equal(tc.expectedPk, pk) { 265 | expected, _ := mc.UnmarshalClientBoundResponse(tc.expectedPk) 266 | received, _ := mc.UnmarshalClientBoundResponse(pk) 267 | t.Errorf("expcted: %#v \ngot: %#v", expected, received) 268 | } 269 | 270 | }) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | filename='ultraviolet' 4 | 5 | if [ -f "$filename" ]; then 6 | echo "Removing old executable" 7 | rm "$filename" 8 | fi 9 | 10 | echo "Build new executable" 11 | go build -o ultraviolet ./cmd/Ultraviolet/ 12 | -------------------------------------------------------------------------------- /script/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | script/build 4 | ./ultraviolet run-experimental -config $UV_CONFIG 5 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go test ./... -timeout 1s -count 1 4 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package ultraviolet 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/pires/go-proxyproto" 8 | "github.com/realDragonium/Ultraviolet/config" 9 | "github.com/realDragonium/Ultraviolet/core" 10 | "github.com/realDragonium/Ultraviolet/mc" 11 | "github.com/realDragonium/Ultraviolet/module" 12 | ) 13 | 14 | var botLimiter = module.NewBotFilterConnLimiter(5, time.Minute, time.Hour, 5*time.Second, mc.Packet{}) 15 | 16 | type ProxyAllServer struct { 17 | } 18 | 19 | func (s ProxyAllServer) ConnAction(req core.RequestData) core.ServerAction { 20 | return core.PROXY 21 | } 22 | 23 | func (s ProxyAllServer) CreateConn(req core.RequestData) (net.Conn, error) { 24 | return &net.TCPConn{}, nil 25 | } 26 | 27 | func (s ProxyAllServer) Status() mc.Packet { 28 | return mc.Packet{} 29 | } 30 | 31 | func NewAPIServer(cfg config.APIServerConfig) core.Server { 32 | dialTimeout, err := time.ParseDuration(cfg.DialTimeout) 33 | if err != nil { 34 | dialTimeout = time.Second 35 | } 36 | 37 | dialer := net.Dialer{ 38 | Timeout: dialTimeout, 39 | LocalAddr: &net.TCPAddr{ 40 | IP: net.ParseIP(cfg.ProxyBind), 41 | }, 42 | } 43 | 44 | disconnectPacket := mc.ClientBoundDisconnect{ 45 | Reason: mc.String(cfg.DisconnectMessage), 46 | }.Marshal() 47 | 48 | cachedStatusPk := cfg.CachedStatus.Marshal() 49 | 50 | serverState := core.Offline 51 | if cfg.IsOnline { 52 | serverState = core.Online 53 | } 54 | 55 | return APIServer{ 56 | sendProxyProtocol: cfg.SendProxyProtocol, 57 | disconnectPacket: disconnectPacket, 58 | dialer: dialer, 59 | useStatusCache: cfg.UseStatusCache, 60 | serverStatusPk: cachedStatusPk, 61 | serverStatus: serverState, 62 | useBotLimiter: cfg.LimitBots, 63 | } 64 | } 65 | 66 | type APIServer struct { 67 | sendProxyProtocol bool 68 | disconnectPacket mc.Packet 69 | 70 | dialer net.Dialer 71 | proxyTo string 72 | 73 | useBotLimiter bool 74 | useStatusCache bool 75 | serverStatusPk mc.Packet 76 | serverStatus core.ServerState 77 | } 78 | 79 | func (server APIServer) ConnAction(req core.RequestData) core.ServerAction { 80 | if server.useBotLimiter && !botLimiter.Allow(req) { 81 | return core.VERIFY_CONN 82 | } 83 | 84 | switch server.serverStatus { 85 | case core.Offline: 86 | return server.serverOffline(req) 87 | case core.Online: 88 | return server.serverOnline(req) 89 | default: 90 | return core.CLOSE 91 | } 92 | } 93 | 94 | func (server APIServer) serverOffline(req core.RequestData) core.ServerAction { 95 | if req.Type == mc.Status { 96 | return core.STATUS 97 | } 98 | 99 | return core.CLOSE 100 | } 101 | 102 | func (server APIServer) serverOnline(req core.RequestData) core.ServerAction { 103 | if req.Type == mc.Login { 104 | // if cfgServer.useRealipv2_4 { 105 | // return PROXY_REALIP_2_4 106 | // } 107 | // if cfgServer.useRealipv2_5 { 108 | // return PROXY_REALIP_2_5 109 | // } 110 | return core.PROXY 111 | } 112 | 113 | if req.Type == mc.Status { 114 | if server.useStatusCache { 115 | return core.STATUS 116 | } 117 | 118 | return core.PROXY 119 | } 120 | 121 | return core.CLOSE 122 | } 123 | 124 | func (server APIServer) CreateConn(req core.RequestData) (conn net.Conn, err error) { 125 | conn, err = server.dialer.Dial("tcp", server.proxyTo) 126 | if err != nil { 127 | return 128 | } 129 | 130 | if server.sendProxyProtocol { 131 | header := &proxyproto.Header{ 132 | Version: 2, 133 | Command: proxyproto.PROXY, 134 | TransportProtocol: proxyproto.TCPv4, 135 | SourceAddr: conn.LocalAddr(), 136 | DestinationAddr: conn.RemoteAddr(), 137 | } 138 | 139 | _, err = header.WriteTo(conn) 140 | } 141 | 142 | return 143 | } 144 | 145 | func (server APIServer) Status() mc.Packet { 146 | return server.serverStatusPk 147 | } 148 | -------------------------------------------------------------------------------- /src/config.go: -------------------------------------------------------------------------------- 1 | package ultravioletv2 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "math/rand" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | MainConfigFileName = "ultraviolet.json" 16 | BedrockConfigFileSuffix = "_bedrock.json" 17 | ) 18 | 19 | type BaseConfig struct { 20 | ListenTo string `json:"listenTo"` 21 | ProxyTo string `json:"proxyTo"` 22 | 23 | // TODO for later: 24 | // - Proxy Protocol options 25 | } 26 | 27 | type JavaConfig struct { 28 | BaseConfig 29 | 30 | Domains []string `json:"domains"` 31 | } 32 | 33 | type BedrockServerConfig struct { 34 | BaseConfig 35 | 36 | ID int64 `json:"id"` 37 | ServerStatus BedrockStatus `json:"status"` 38 | } 39 | 40 | func (cfg BedrockServerConfig) Status() string { 41 | s := cfg.ServerStatus 42 | return fmt.Sprintf("%s;%s;%d;%s;%d;%d;%d;%s;%s;%d;%d;%d", s.Edition, 43 | s.Description.Text, s.Version.Protocol, s.Version.Name, s.Players.Online, 44 | s.Players.Max, cfg.ID, s.Description.Text_2, s.Gamemode.Name, s.Gamemode.ID, 45 | s.Port.IPv4, s.Port.IPv6) 46 | } 47 | 48 | func StringToBedrockStatus(s string) (status BedrockStatus) { 49 | parts := strings.Split(s, ";") 50 | 51 | status.Edition = parts[0] 52 | status.Description.Text = parts[1] 53 | status.Version.Protocol, _ = strconv.Atoi(parts[2]) 54 | status.Version.Name = parts[3] 55 | status.Players.Online, _ = strconv.Atoi(parts[4]) 56 | status.Players.Max, _ = strconv.Atoi(parts[5]) 57 | // Dont convert ServerGUID 58 | status.Description.Text_2 = parts[7] 59 | status.Gamemode.Name = parts[8] 60 | status.Gamemode.ID, _ = strconv.Atoi(parts[9]) 61 | status.Port.IPv4, _ = strconv.Atoi(parts[10]) 62 | status.Port.IPv6, _ = strconv.Atoi(parts[11]) 63 | return 64 | } 65 | 66 | type BedrockStatus struct { 67 | Edition string `json:"Edition"` 68 | Description Description `json:"Description"` 69 | Version Version `json:"version"` 70 | Players Players `json:"players"` 71 | Gamemode GameMode `json:"gamemode"` 72 | Port Port `json:"port"` 73 | } 74 | 75 | type GameMode struct { 76 | Name string `json:"name"` 77 | ID int `json:"id"` 78 | } 79 | 80 | type Port struct { 81 | IPv4 int `json:"ipv4"` 82 | IPv6 int `json:"ipv6"` 83 | } 84 | 85 | type JavaStatus struct { 86 | Version Version `json:"version"` 87 | Players Players `json:"players"` 88 | Description Description `json:"description"` 89 | Favicon string `json:"favicon"` 90 | } 91 | 92 | type Version struct { 93 | Name string `json:"name"` 94 | Protocol int `json:"protocol"` 95 | } 96 | 97 | type Players struct { 98 | Max int `json:"max"` 99 | Online int `json:"online"` 100 | } 101 | 102 | type Description struct { 103 | Text string `json:"text"` 104 | Text_2 string `json:"text_2"` 105 | } 106 | 107 | func ReadBedrockConfigs(path string) (cfgs []BedrockServerConfig, err error) { 108 | var filePaths []string 109 | err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 110 | if err != nil { 111 | return err 112 | } 113 | if info.IsDir() { 114 | return nil 115 | } 116 | if !strings.Contains(path, BedrockConfigFileSuffix) { 117 | return nil 118 | } 119 | filePaths = append(filePaths, path) 120 | return nil 121 | }) 122 | 123 | if err != nil { 124 | return 125 | } 126 | 127 | for _, filePath := range filePaths { 128 | cfg, err := LoadBedrockServerConfig(filePath) 129 | if err != nil { 130 | return nil, err 131 | } 132 | cfgs = append(cfgs, cfg) 133 | } 134 | 135 | return cfgs, nil 136 | } 137 | 138 | func LoadBedrockServerConfig(path string) (cfg BedrockServerConfig, err error) { 139 | bb, err := os.ReadFile(path) 140 | if err != nil { 141 | return 142 | } 143 | 144 | cfg = DefaultBedrockServerConfig() 145 | if err := json.Unmarshal(bb, &cfg); err != nil { 146 | return cfg, err 147 | } 148 | 149 | if cfg.BaseConfig.ListenTo == "" || cfg.BaseConfig.ProxyTo == "" { 150 | return cfg, fmt.Errorf("ListenTo and ProxyTo must be set") 151 | } 152 | 153 | port := strings.SplitAfter(cfg.ListenTo, ":")[1] 154 | cfg.ServerStatus.Port.IPv4, err = strconv.Atoi(port) 155 | if err != nil { 156 | return cfg, err 157 | } 158 | 159 | return cfg, nil 160 | } 161 | 162 | func DefaultBedrockServerConfig() BedrockServerConfig { 163 | return BedrockServerConfig{ 164 | ID: rand.Int63(), 165 | ServerStatus: BedrockStatus{ 166 | Edition: "MCPE", 167 | Description: Description{ 168 | Text: "Proxied with Ultraviolet", 169 | }, 170 | Version: Version{ 171 | Name: "1.19.10", 172 | Protocol: 534, 173 | }, 174 | Players: Players{ 175 | Online: 0, 176 | Max: 100, 177 | }, 178 | Gamemode: GameMode{ 179 | Name: "Survival", 180 | ID: 1, 181 | }, 182 | Port: Port{ 183 | IPv4: -1, 184 | IPv6: -1, 185 | }, 186 | }, 187 | } 188 | } 189 | 190 | func ReadJavaConfigs(path string) (cfgs []JavaConfig, err error) { 191 | var filePaths []string 192 | err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 193 | if err != nil { 194 | return err 195 | } 196 | if info.IsDir() { 197 | return nil 198 | } 199 | if strings.Contains(path, BedrockConfigFileSuffix) { 200 | return nil 201 | } 202 | if filepath.Ext(path) != ".json" { 203 | return nil 204 | } 205 | if info.Name() == MainConfigFileName { 206 | return nil 207 | } 208 | filePaths = append(filePaths, path) 209 | return nil 210 | }) 211 | 212 | if err != nil { 213 | return 214 | } 215 | 216 | for _, filePath := range filePaths { 217 | cfg, err := LoadJavaServerConfig(filePath) 218 | if err != nil { 219 | return nil, err 220 | } 221 | cfgs = append(cfgs, cfg) 222 | } 223 | 224 | return cfgs, nil 225 | } 226 | 227 | func LoadJavaServerConfig(path string) (JavaConfig, error) { 228 | bb, err := os.ReadFile(path) 229 | if err != nil { 230 | return JavaConfig{}, err 231 | } 232 | 233 | cfg := DefaultJavaConfig() 234 | if err := json.Unmarshal(bb, &cfg); err != nil { 235 | return cfg, err 236 | } 237 | 238 | if cfg.ListenTo == "" || cfg.ProxyTo == "" { 239 | return cfg, errors.New("ListenTo and ProxyTo must be set") 240 | } 241 | 242 | return cfg, nil 243 | } 244 | 245 | func DefaultJavaConfig() JavaConfig { 246 | return JavaConfig{} 247 | } 248 | -------------------------------------------------------------------------------- /src/config_test.go: -------------------------------------------------------------------------------- 1 | package ultravioletv2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ultravioletv2 "github.com/realDragonium/Ultraviolet/src" 7 | ) 8 | 9 | func TestBedrockStatus(t *testing.T) { 10 | bedrock_status := "MCPE;This Server - UV;534;1.19.10;0;100;92837498723498;MOTD_2;Survival;1;19132;-1" 11 | 12 | cfg := ultravioletv2.BedrockServerConfig{ 13 | ID: 92837498723498, 14 | ServerStatus: ultravioletv2.BedrockStatus{ 15 | Edition: "MCPE", 16 | Description: ultravioletv2.Description{ 17 | Text: "This Server - UV", 18 | Text_2: "MOTD_2", 19 | }, 20 | Version: ultravioletv2.Version{ 21 | Name: "1.19.10", 22 | Protocol: 534, 23 | }, 24 | Players: ultravioletv2.Players{ 25 | Online: 0, 26 | Max: 100, 27 | }, 28 | Gamemode: ultravioletv2.GameMode{ 29 | Name: "Survival", 30 | ID: 1, 31 | }, 32 | Port: ultravioletv2.Port{ 33 | IPv4: 19132, 34 | IPv6: -1, 35 | }, 36 | }, 37 | } 38 | 39 | convertedStatus := ultravioletv2.StringToBedrockStatus(bedrock_status) 40 | if cfg.ServerStatus != convertedStatus { 41 | t.Errorf("Expected %#v, got %#v", cfg.ServerStatus, convertedStatus) 42 | } 43 | 44 | if cfg.Status() != bedrock_status { 45 | t.Errorf("Expected %#v, got %#v", bedrock_status, cfg.Status()) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/packet.go: -------------------------------------------------------------------------------- 1 | package ultravioletv2 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | "log" 8 | ) 9 | 10 | type ServerBoundHandshakePacket struct { 11 | PacketId byte 12 | ProtocolVersion int 13 | ServerAddress string 14 | ServerPort int16 15 | NextState int 16 | } 17 | 18 | func (pk ServerBoundHandshakePacket) WriteTo(w io.Writer) (int64, error) { 19 | buf := bytes.NewBuffer([]byte{}) 20 | 21 | WriteByte(buf, pk.PacketId) 22 | l := 1 // len of byte 23 | m, _ := WriteVarInt(buf, pk.ProtocolVersion) 24 | n, _ := WriteString(buf, pk.ServerAddress) 25 | WriteShort(buf, pk.ServerPort) 26 | o := 2 // len of short 27 | p, _ := WriteVarInt(buf, pk.NextState) 28 | 29 | pkLen := l + m + n + o + p 30 | 31 | q, _ := WriteVarInt(w, pkLen) 32 | buf.WriteTo(w) 33 | 34 | pkLen += q 35 | return int64(pkLen), nil 36 | } 37 | 38 | func ReadServerBoundHandshake(r io.Reader) (pk ServerBoundHandshakePacket, err error) { 39 | pk.PacketId, err = ReadByte(r) 40 | if err != nil { 41 | return 42 | } 43 | 44 | pk.ProtocolVersion, err = ReadVarInt(r) 45 | if err != nil { 46 | return 47 | } 48 | 49 | pk.ServerAddress, err = ReadString(r) 50 | if err != nil { 51 | return 52 | } 53 | 54 | pk.ServerPort, err = ReadShort(r) 55 | if err != nil { 56 | return 57 | } 58 | 59 | pk.NextState, err = ReadVarInt(r) 60 | if err != nil { 61 | return 62 | } 63 | 64 | return 65 | } 66 | 67 | // UnconnectedMessageSequence is a sequence of bytes which is found in every unconnected message sent in RakNet. 68 | var UnconnectedMessageSequence = [16]byte{0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, 0x56, 0x78} 69 | 70 | type UnconnectedPing struct { 71 | SendTimestamp int64 72 | Magic [16]byte 73 | ClientGUID int64 74 | } 75 | 76 | func (pk *UnconnectedPing) Write(w io.Writer) (err error) { 77 | buf := bytes.NewBuffer([]byte{}) 78 | err = binary.Write(buf, binary.BigEndian, IDUnconnectedPing) 79 | if err != nil { 80 | log.Printf("Error writing IDUnconnectedPing: %v", err) 81 | } 82 | err = binary.Write(buf, binary.BigEndian, pk.SendTimestamp) 83 | if err != nil { 84 | log.Printf("Error writing SendTimestamp: %v", err) 85 | } 86 | err = binary.Write(buf, binary.BigEndian, UnconnectedMessageSequence) 87 | if err != nil { 88 | log.Printf("Error writing unconnectedMessageSequence: %v", err) 89 | } 90 | err = binary.Write(buf, binary.BigEndian, pk.ClientGUID) 91 | if err != nil { 92 | log.Printf("Error writing ClientGUID: %v", err) 93 | } 94 | 95 | w.Write(buf.Bytes()) 96 | 97 | return 98 | } 99 | 100 | func (pk *UnconnectedPing) Read(r io.Reader) error { 101 | _ = binary.Read(r, binary.BigEndian, &pk.SendTimestamp) 102 | _ = binary.Read(r, binary.BigEndian, &pk.Magic) 103 | return binary.Read(r, binary.BigEndian, &pk.ClientGUID) 104 | } 105 | 106 | type UnconnectedPong struct { 107 | SendTimestamp int64 108 | Magic [16]byte 109 | ServerGUID int64 110 | Data string 111 | } 112 | 113 | func (pk *UnconnectedPong) Write(w io.Writer) { 114 | _ = binary.Write(w, binary.BigEndian, IDUnconnectedPong) 115 | _ = binary.Write(w, binary.BigEndian, pk.SendTimestamp) 116 | _ = binary.Write(w, binary.BigEndian, pk.ServerGUID) 117 | _ = binary.Write(w, binary.BigEndian, UnconnectedMessageSequence) 118 | _ = binary.Write(w, binary.BigEndian, int16(len(pk.Data))) 119 | _ = binary.Write(w, binary.BigEndian, []byte(pk.Data)) 120 | } 121 | 122 | func (pk *UnconnectedPong) Read(r io.Reader) error { 123 | var l int16 124 | _ = binary.Read(r, binary.BigEndian, &pk.SendTimestamp) 125 | _ = binary.Read(r, binary.BigEndian, &pk.ServerGUID) 126 | _ = binary.Read(r, binary.BigEndian, &pk.Magic) 127 | _ = binary.Read(r, binary.BigEndian, &l) 128 | readB := make([]byte, l) 129 | n, err := r.Read(readB) 130 | pk.Data = string(readB[:n]) 131 | return err 132 | } 133 | -------------------------------------------------------------------------------- /src/packet_test.go: -------------------------------------------------------------------------------- 1 | package ultravioletv2_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/realDragonium/Ultraviolet/mc" 8 | ultravioletv2 "github.com/realDragonium/Ultraviolet/src" 9 | ) 10 | 11 | func TestServerBoundHandshak(t *testing.T) { 12 | tt := []struct { 13 | packet ultravioletv2.ServerBoundHandshakePacket 14 | rawPacket []byte 15 | expectedRawData []byte 16 | }{ 17 | { 18 | rawPacket: []byte{0x00, 0xC2, 0x04, 0x0B, 0x73, 0x70, 0x6F, 0x6F, 0x6B, 0x2E, 0x73, 0x70, 0x61, 0x63, 0x65, 0x63, 0xDD, 0x01}, 19 | expectedRawData: []byte{0x12, 0x00, 0xC2, 0x04, 0x0B, 0x73, 0x70, 0x6F, 0x6F, 0x6B, 0x2E, 0x73, 0x70, 0x61, 0x63, 0x65, 0x63, 0xDD, 0x01}, 20 | packet: ultravioletv2.ServerBoundHandshakePacket{ 21 | PacketId: 0x00, 22 | ProtocolVersion: 578, 23 | ServerAddress: "spook.space", 24 | ServerPort: 25565, 25 | NextState: mc.StatusState, 26 | }, 27 | }, 28 | { 29 | rawPacket: []byte{0x00, 0xC2, 0x04, 0x0B, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x05, 0x39, 0x01}, 30 | expectedRawData: []byte{0x12, 0x00, 0xC2, 0x04, 0x0B, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x05, 0x39, 0x01}, 31 | packet: ultravioletv2.ServerBoundHandshakePacket{ 32 | PacketId: 0x00, 33 | ProtocolVersion: 578, 34 | ServerAddress: "example.com", 35 | ServerPort: 1337, 36 | NextState: mc.StatusState, 37 | }, 38 | }, 39 | } 40 | 41 | for _, tc := range tt { 42 | reader := bytes.NewReader(tc.rawPacket) 43 | pk, err := ultravioletv2.ReadServerBoundHandshake(reader) 44 | 45 | if err != nil { 46 | t.Errorf("Error reading packet: %v", err) 47 | } 48 | 49 | if pk.ProtocolVersion != tc.packet.ProtocolVersion { 50 | t.Errorf("Expected protocol version %d, got %d", tc.packet.ProtocolVersion, pk.ProtocolVersion) 51 | } 52 | 53 | if pk.ServerAddress != tc.packet.ServerAddress { 54 | t.Errorf("Expected server address %s, got %s", tc.packet.ServerAddress, pk.ServerAddress) 55 | } 56 | 57 | if pk.ServerPort != tc.packet.ServerPort { 58 | t.Errorf("Expected server port %d, got %d", tc.packet.ServerPort, pk.ServerPort) 59 | } 60 | 61 | if pk.NextState != tc.packet.NextState { 62 | t.Errorf("Expected next state %d, got %d", tc.packet.NextState, pk.NextState) 63 | } 64 | 65 | buf := bytes.NewBuffer(nil) 66 | n, err := pk.WriteTo(buf) 67 | if err != nil { 68 | t.Errorf("Error writing packet: %v", err) 69 | } 70 | 71 | if n != int64(len(tc.expectedRawData)) { 72 | t.Errorf("Expected %d bytes, got %d", len(tc.rawPacket), n) 73 | } 74 | 75 | if !bytes.Equal(buf.Bytes(), tc.expectedRawData) { 76 | t.Errorf("Expected packet %v, got %v", tc.rawPacket, buf.Bytes()) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/server.go: -------------------------------------------------------------------------------- 1 | package ultravioletv2 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | 10 | "github.com/realDragonium/Ultraviolet/config" 11 | "github.com/realDragonium/Ultraviolet/core" 12 | ) 13 | 14 | var ( 15 | UVConfig config.UltravioletConfig = config.UltravioletConfig{ 16 | ListenTo: ":25565", 17 | } 18 | Servers map[string]string = map[string]string{} 19 | connections map[string]net.Conn = map[string]net.Conn{} 20 | ) 21 | 22 | func Run(cfgPath string) error { 23 | log.Println("Going to run!") 24 | 25 | bedrockServerConfigs, err := ReadBedrockConfigs(cfgPath) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | for _, cfg := range bedrockServerConfigs { 31 | listener, err := CreateBedrockListener(cfg) 32 | if err != nil { 33 | return err 34 | } 35 | go StartBedrockServer(listener, cfg) 36 | } 37 | 38 | javaServerConfigs, err := ReadJavaConfigs(cfgPath) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | for _, cfg := range javaServerConfigs { 44 | for _, domain := range cfg.Domains { 45 | Servers[domain] = cfg.ListenTo 46 | } 47 | } 48 | 49 | go BasicProxySetupJava() 50 | 51 | log.Println("Finished starting up") 52 | select {} 53 | } 54 | 55 | const ( 56 | IDUnconnectedPing byte = 0x01 57 | IDUnconnectedPingOpenConnections byte = 0x02 58 | IDUnconnectedPong byte = 0x1c 59 | ) 60 | 61 | func CreateBedrockListener(cfg BedrockServerConfig) (net.PacketConn, error) { 62 | listener, err := net.ListenPacket("udp", cfg.ListenTo) 63 | if err != nil { 64 | log.Panicf("Error listening to udp: %v", err) 65 | return listener, err 66 | } 67 | return listener, nil 68 | } 69 | 70 | func StartBedrockServer(listener net.PacketConn, cfg BedrockServerConfig) error { 71 | buf := make([]byte, 2048) 72 | for { 73 | n, addr, err := listener.ReadFrom(buf) 74 | if err != nil { 75 | return err 76 | } 77 | go serve(cfg, listener, addr, buf[:n]) 78 | } 79 | } 80 | 81 | func serve(cfg BedrockServerConfig, pc net.PacketConn, addr net.Addr, bb []byte) error { 82 | buf := bytes.NewBuffer(bb) 83 | packetID, _ := buf.ReadByte() 84 | 85 | switch packetID { 86 | case IDUnconnectedPing, IDUnconnectedPingOpenConnections: 87 | return handleUnconnectedPing(cfg, pc, addr, buf) 88 | default: 89 | udpAddr := addr.(*net.UDPAddr) 90 | ip := udpAddr.IP.String() 91 | 92 | serverConn, ok := connections[ip] 93 | if !ok { 94 | serverConn, _ = net.Dial("udp", cfg.ProxyTo) 95 | connections[ip] = serverConn 96 | 97 | go func() { 98 | for { 99 | b := make([]byte, 2048) 100 | n, err := serverConn.Read(b) 101 | if err != nil { 102 | break 103 | } 104 | pc.WriteTo(b[:n], addr) 105 | } 106 | }() 107 | } 108 | serverConn.Write(bb) 109 | } 110 | 111 | return nil 112 | } 113 | 114 | func handleUnconnectedPing(cfg BedrockServerConfig, pc net.PacketConn, addr net.Addr, b *bytes.Buffer) error { 115 | pk := &UnconnectedPing{} 116 | if err := pk.Read(b); err != nil { 117 | return fmt.Errorf("error reading unconnected ping: %v", err) 118 | } 119 | b.Reset() 120 | pong := UnconnectedPong{ServerGUID: cfg.ID, SendTimestamp: pk.SendTimestamp, Data: cfg.Status()} 121 | (&pong).Write(b) 122 | pc.WriteTo(b.Bytes(), addr) 123 | 124 | return nil 125 | } 126 | 127 | func BasicProxySetupJava() error { 128 | ln, err := CreateListener(UVConfig) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | for { 134 | conn, err := ln.Accept() 135 | if err != nil { 136 | return err 137 | } 138 | 139 | ProcessConnection(conn) 140 | } 141 | } 142 | 143 | func CreateListener(cfg config.UltravioletConfig) (net.Listener, error) { 144 | return net.Listen("tcp", cfg.ListenTo) 145 | } 146 | 147 | func ProcessConnection(conn net.Conn) error { 148 | _, data, err := ReadPacketData(conn) 149 | if err != nil { 150 | log.Printf("Error reading packet data: %v", err) 151 | return err 152 | } 153 | 154 | r := bytes.NewReader(data) 155 | hsPacket, _ := ReadServerBoundHandshake(r) 156 | 157 | server, _ := ConnectToServer(hsPacket) 158 | return ProxyConnection(conn, server) 159 | } 160 | 161 | func ReadPacketData(r io.Reader) (int, []byte, error) { 162 | packetLength, _ := ReadVarInt(r) 163 | 164 | if packetLength < 1 { 165 | return packetLength, []byte{}, nil 166 | } 167 | 168 | data := make([]byte, packetLength) 169 | 170 | if _, err := r.Read(data); err != nil { 171 | log.Println("got error during reading of bytes: ", err) 172 | return 0, data, err 173 | } 174 | 175 | return packetLength, data, nil 176 | } 177 | 178 | func ConnectToServer(pk ServerBoundHandshakePacket) (net.Conn, error) { 179 | addr, _ := ServerAddress(pk) 180 | 181 | server, err := net.Dial("tcp", addr) 182 | if err != nil { 183 | log.Println("Error connecting to server:", err) 184 | return nil, err 185 | } 186 | 187 | pk.WriteTo(server) 188 | 189 | return server, err 190 | } 191 | 192 | func ServerAddress(hsPacket ServerBoundHandshakePacket) (string, error) { 193 | serverAddr, ok := Servers[hsPacket.ServerAddress] 194 | 195 | if !ok { 196 | return "", core.ErrNoServerFound 197 | } 198 | 199 | return serverAddr, nil 200 | } 201 | 202 | func ProxyConnection(client, server net.Conn) error { 203 | go func() { 204 | pipe(server, client) 205 | client.Close() 206 | }() 207 | pipe(client, server) 208 | server.Close() 209 | 210 | return nil 211 | } 212 | 213 | func pipe(c1, c2 net.Conn) { 214 | buffer := make([]byte, 0xffff) 215 | for { 216 | n, err := c1.Read(buffer) 217 | if err != nil { 218 | return 219 | } 220 | _, err = c2.Write(buffer[:n]) 221 | if err != nil { 222 | return 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/server_test.go: -------------------------------------------------------------------------------- 1 | package ultravioletv2_test 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/realDragonium/Ultraviolet/config" 11 | "github.com/realDragonium/Ultraviolet/core" 12 | ultravioletv2 "github.com/realDragonium/Ultraviolet/src" 13 | ) 14 | 15 | // timestamp returns a timestamp in milliseconds. 16 | func timestamp() int64 { 17 | return time.Now().UnixNano() / int64(time.Second) 18 | } 19 | 20 | func TestConnectToServer(t *testing.T) { 21 | listener, err := net.Listen("tcp", "") 22 | if err != nil { 23 | t.Fatalf("Error during creating listener: %v", err) 24 | } 25 | 26 | ultravioletv2.Servers = map[string]string{ 27 | "localhost": listener.Addr().String(), 28 | } 29 | 30 | pk := ultravioletv2.ServerBoundHandshakePacket{ 31 | ServerAddress: "localhost", 32 | } 33 | 34 | go ultravioletv2.ConnectToServer(pk) 35 | 36 | conn, err := listener.Accept() 37 | if err != nil { 38 | t.Fatalf("Error during accepting connection: %v", err) 39 | } 40 | 41 | n, data, err := ultravioletv2.ReadPacketData(conn) 42 | 43 | if err != nil { 44 | t.Fatalf("Error during reading packet data: %v", err) 45 | } 46 | 47 | if n != len(data) { 48 | t.Errorf("Expected packet length: %d, got %d", len(data), n) 49 | } 50 | 51 | r := bytes.NewReader(data) 52 | receivedPk, _ := ultravioletv2.ReadServerBoundHandshake(r) 53 | 54 | if !cmp.Equal(pk, receivedPk) { 55 | t.Errorf("Expected packet: %#v, got %#v", pk, receivedPk) 56 | } 57 | } 58 | 59 | func TestFullProxyConnection(t *testing.T) { 60 | 61 | } 62 | 63 | func TestCreateListener(t *testing.T) { 64 | cfg := config.UltravioletConfig{ 65 | ListenTo: ":0", 66 | } 67 | 68 | ln, err := ultravioletv2.CreateListener(cfg) 69 | if err != nil { 70 | t.Fatalf("Error during creating listener: %v", err) 71 | } 72 | 73 | go func() { 74 | net.Dial("tcp", ln.Addr().String()) 75 | }() 76 | 77 | conn, err := ln.Accept() 78 | if err != nil { 79 | t.Fatalf("Error during accepting connection: %v", err) 80 | } 81 | 82 | if conn == nil { 83 | t.Fatal("Expected connection, got nil") 84 | } 85 | } 86 | 87 | func TestServerAddress(t *testing.T) { 88 | tt := []struct { 89 | address string 90 | registeredAddress string 91 | proxyToAddress string 92 | expectedError error 93 | }{ 94 | { 95 | address: "localhost", 96 | registeredAddress: "localhost", 97 | proxyToAddress: "localhost:25566", 98 | }, 99 | { 100 | address: "local", 101 | registeredAddress: "localhost", 102 | expectedError: core.ErrNoServerFound, 103 | }, 104 | } 105 | 106 | for _, tc := range tt { 107 | ultravioletv2.Servers = map[string]string{ 108 | tc.registeredAddress: tc.proxyToAddress, 109 | } 110 | 111 | pk := ultravioletv2.ServerBoundHandshakePacket{ 112 | ProtocolVersion: 0, 113 | ServerAddress: tc.address, 114 | ServerPort: 0, 115 | NextState: 0, 116 | } 117 | 118 | serverAddr, err := ultravioletv2.ServerAddress(pk) 119 | 120 | if err != nil { 121 | if tc.expectedError == nil { 122 | t.Errorf("Error getting server address: %v", err) 123 | continue 124 | } 125 | 126 | if err != tc.expectedError { 127 | t.Errorf("Expected error %v, got %v", tc.expectedError, err) 128 | } 129 | } 130 | 131 | if err != tc.expectedError { 132 | t.Fatalf("Expected error %v, got %v", tc.expectedError, err) 133 | } 134 | 135 | if err == nil && serverAddr != tc.proxyToAddress { 136 | t.Errorf("Expected server address: '%s', got '%s'", tc.proxyToAddress, serverAddr) 137 | } 138 | 139 | } 140 | 141 | } 142 | 143 | func TestBedrockServerListener(t *testing.T) { 144 | config := ultravioletv2.BedrockServerConfig{ 145 | BaseConfig: ultravioletv2.BaseConfig{ 146 | ListenTo: ":0", 147 | ProxyTo: "", 148 | }, 149 | } 150 | 151 | ln, err := ultravioletv2.CreateBedrockListener(config) 152 | if err != nil { 153 | t.Errorf("Error creating listener: %v", err) 154 | } 155 | 156 | if ln == nil { 157 | t.Error("Expected listener, got nil") 158 | } 159 | 160 | } 161 | 162 | func TestBedrockServer(t *testing.T) { 163 | config := ultravioletv2.BedrockServerConfig{ 164 | BaseConfig: ultravioletv2.BaseConfig{ 165 | ListenTo: ":0", 166 | ProxyTo: "", 167 | }, 168 | ID: 23894692837498, 169 | ServerStatus: ultravioletv2.BedrockStatus{ 170 | Edition: "MCPE", 171 | Description: ultravioletv2.Description{Text: "This Server - UV"}, 172 | Version: ultravioletv2.Version{ 173 | Name: "1.19.10", 174 | Protocol: 534, 175 | }, 176 | Players: ultravioletv2.Players{ 177 | Online: 0, 178 | Max: 100, 179 | }, 180 | Gamemode: ultravioletv2.GameMode{ 181 | Name: "Survival", 182 | ID: 1, 183 | }, 184 | Port: ultravioletv2.Port{ 185 | IPv4: 19132, 186 | IPv6: -1, 187 | }, 188 | }, 189 | } 190 | 191 | ln, err := ultravioletv2.CreateBedrockListener(config) 192 | if err != nil { 193 | t.Errorf("Error creating listener: %v", err) 194 | } 195 | 196 | go ultravioletv2.StartBedrockServer(ln, config) 197 | 198 | pingPk := ultravioletv2.UnconnectedPing{ 199 | ClientGUID: 23794873294, 200 | Magic: ultravioletv2.UnconnectedMessageSequence, 201 | SendTimestamp: timestamp(), 202 | } 203 | 204 | conn, err := net.Dial("udp", ln.LocalAddr().String()) 205 | if err != nil { 206 | t.Errorf("Error dialing server: %v", err) 207 | } 208 | 209 | err = pingPk.Write(conn) 210 | if err != nil { 211 | t.Errorf("Error writing packet: %v", err) 212 | } 213 | 214 | readBytes := make([]byte, 2048) 215 | n, err := conn.Read(readBytes) 216 | if err != nil { 217 | t.Errorf("Error reading packet: %v", err) 218 | } 219 | 220 | buf := bytes.NewReader(readBytes[:n]) 221 | buf.ReadByte() // Skip packet id 222 | pongPk := ultravioletv2.UnconnectedPong{} 223 | pongPk.Read(buf) 224 | 225 | if pongPk.Magic != ultravioletv2.UnconnectedMessageSequence { 226 | t.Errorf("Expected magic: %d, got %d", ultravioletv2.UnconnectedMessageSequence, pongPk.Magic) 227 | } 228 | 229 | if pongPk.ServerGUID != config.ID { 230 | t.Errorf("Expected server GUID: %d, got %d", config.ID, pongPk.ServerGUID) 231 | } 232 | 233 | if pongPk.SendTimestamp != pingPk.SendTimestamp { 234 | t.Errorf("Expected send timestamp: %d, got %d", pingPk.SendTimestamp, pongPk.SendTimestamp) 235 | } 236 | 237 | if pongPk.Data != config.Status() { 238 | t.Errorf("Expected status: %s, got %s", config.Status(), pongPk.Data) 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /src/type.go: -------------------------------------------------------------------------------- 1 | package ultravioletv2 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | func ReadVarInt(r io.Reader) (v int, err error) { 9 | for i := 0; ; i++ { 10 | var b byte 11 | b, err = ReadByte(r) 12 | if err != nil { 13 | return 0, err 14 | } 15 | 16 | v |= int(b&0x7f) << uint(7*i) 17 | if b&0x80 == 0 { 18 | break 19 | } 20 | } 21 | return 22 | } 23 | 24 | func WriteVarInt(w io.Writer, varInt int) (int, error) { 25 | var bb []byte 26 | for { 27 | b := varInt & 0x7F 28 | varInt >>= 7 29 | if varInt != 0 { 30 | b |= 0x80 31 | } 32 | bb = append(bb, byte(b)) 33 | if varInt == 0 { 34 | break 35 | } 36 | } 37 | 38 | return w.Write(bb) 39 | } 40 | 41 | func ReadByte(r io.Reader) (b byte, err error) { 42 | binary.Read(r, binary.BigEndian, &b) 43 | return 44 | } 45 | 46 | func WriteByte(w io.Writer, b byte) error { 47 | return binary.Write(w, binary.BigEndian, b) 48 | } 49 | 50 | func ReadString(r io.Reader) (string, error) { 51 | length, err := ReadVarInt(r) 52 | if err != nil { 53 | return "", err 54 | } 55 | 56 | bb := make([]byte, length) 57 | _, err = r.Read(bb) 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | return string(bb), nil 63 | } 64 | 65 | func WriteString(w io.Writer, s string) (int, error) { 66 | length := len(s) 67 | m, err := WriteVarInt(w, length) 68 | if err != nil { 69 | return 0, err 70 | } 71 | 72 | n, err := w.Write([]byte(s)) 73 | n += m 74 | return n, err 75 | } 76 | 77 | func ReadShort(r io.Reader) (bb int16, err error) { 78 | err = binary.Read(r, binary.BigEndian, &bb) 79 | return 80 | } 81 | 82 | func WriteShort(w io.Writer, short int16) error { 83 | return binary.Write(w, binary.BigEndian, short) 84 | } 85 | -------------------------------------------------------------------------------- /worker/api.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | ultraviolet "github.com/realDragonium/Ultraviolet" 8 | ) 9 | 10 | func NewAPI(backendManager BackendManager) ultraviolet.API { 11 | return &api{ 12 | backendManager: backendManager, 13 | } 14 | } 15 | 16 | type api struct { 17 | backendManager BackendManager 18 | server http.Server 19 | } 20 | 21 | func (api *api) Close() { 22 | api.server.Close() 23 | } 24 | 25 | func (api *api) Run(addr string) { 26 | mux := http.NewServeMux() 27 | mux.HandleFunc("/reload", api.reloadHandler) 28 | api.server = http.Server{Addr: addr, Handler: mux} 29 | api.server.ListenAndServe() 30 | } 31 | 32 | func (api *api) reloadHandler(w http.ResponseWriter, r *http.Request) { 33 | err := api.backendManager.Update() 34 | if err != nil { 35 | http.Error(w, err.Error(), 500) 36 | return 37 | } 38 | w.WriteHeader(200) 39 | fmt.Fprintln(w, "success") 40 | } 41 | -------------------------------------------------------------------------------- /worker/backend.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/pires/go-proxyproto" 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/promauto" 9 | "github.com/realDragonium/Ultraviolet/config" 10 | "github.com/realDragonium/Ultraviolet/core" 11 | "github.com/realDragonium/Ultraviolet/mc" 12 | "github.com/realDragonium/Ultraviolet/module" 13 | ) 14 | 15 | type CheckOpenConns struct { 16 | Ch chan bool 17 | } 18 | 19 | var ( 20 | playersConnected = promauto.NewGaugeVec(prometheus.GaugeOpts{ 21 | Namespace: "ultraviolet", 22 | Name: "player_connected", 23 | Help: "The total number of connected players", 24 | }, []string{"host"}) 25 | ) 26 | 27 | type BackendFactoryFunc func(config.BackendWorkerConfig) Backend 28 | 29 | type Backend interface { 30 | ReqCh() chan<- BackendRequest 31 | HasActiveConn() bool 32 | Update(cfg BackendConfig) error 33 | Close() 34 | Server() core.Server 35 | } 36 | 37 | func NewBackendConfig(cfg config.BackendWorkerConfig) BackendConfig { 38 | var connCreator module.ConnectionCreator 39 | var hsModifier module.HandshakeModifier 40 | var rateLimiter module.ConnectionLimiter 41 | var statusCache module.StatusCache 42 | var serverState module.StateAgent 43 | 44 | dialer := net.Dialer{ 45 | Timeout: cfg.DialTimeout, 46 | LocalAddr: &net.TCPAddr{ 47 | IP: net.ParseIP(cfg.ProxyBind), 48 | }, 49 | } 50 | connCreator = module.BasicConnCreator(cfg.ProxyTo, dialer) 51 | 52 | if cfg.RateLimit > 0 { 53 | unverifyCooldown := 10 * cfg.RateLimitDuration 54 | rateLimiter = module.NewBotFilterConnLimiter(cfg.RateLimit, cfg.RateLimitDuration, unverifyCooldown, cfg.RateBanListCooldown, cfg.RateDisconPk) 55 | } else { 56 | rateLimiter = module.AlwaysAllowConnection{} 57 | } 58 | 59 | if cfg.CacheStatus { 60 | statusCache = module.NewStatusCache(cfg.ValidProtocol, cfg.CacheUpdateCooldown, connCreator) 61 | } 62 | 63 | switch cfg.StateOption { 64 | case config.ALWAYS_ONLINE: 65 | serverState = module.AlwaysOnlineState{} 66 | case config.ALWAYS_OFFLINE: 67 | serverState = module.AlwaysOfflineState{} 68 | case config.CACHE: 69 | fallthrough 70 | default: 71 | serverState = module.NewMcServerState(cfg.StateUpdateCooldown, connCreator) 72 | } 73 | 74 | if cfg.OldRealIp { 75 | hsModifier = module.NewRealIP2_4() 76 | } else if cfg.NewRealIP { 77 | hsModifier = module.NewRealIP2_5(cfg.RealIPKey) 78 | } 79 | 80 | return BackendConfig{ 81 | Name: cfg.Name, 82 | UpdateProxyProtocol: true, 83 | SendProxyProtocol: cfg.SendProxyProtocol, 84 | 85 | DisconnectPacket: cfg.DisconnectPacket, 86 | OfflineStatusPacket: cfg.OfflineStatus, 87 | 88 | ConnCreator: connCreator, 89 | HsModifier: hsModifier, 90 | ConnLimiter: rateLimiter, 91 | ServerState: serverState, 92 | StatusCache: statusCache, 93 | } 94 | } 95 | 96 | type BackendConfig struct { 97 | Name string 98 | UpdateProxyProtocol bool 99 | SendProxyProtocol bool 100 | DisconnectPacket mc.Packet 101 | OfflineStatusPacket mc.Packet 102 | 103 | HsModifier module.HandshakeModifier 104 | ConnCreator module.ConnectionCreator 105 | ConnLimiter module.ConnectionLimiter 106 | ServerState module.StateAgent 107 | StatusCache module.StatusCache 108 | } 109 | 110 | var BackendFactory BackendFactoryFunc = func(cfg config.BackendWorkerConfig) Backend { 111 | backendwrk := NewBackendWorker(cfg) 112 | backendwrk.Run() 113 | return &backendwrk 114 | } 115 | 116 | func NewBackendWorker(cfgServer config.BackendWorkerConfig) BackendWorker { 117 | cfg := NewBackendConfig(cfgServer) 118 | cfg.UpdateProxyProtocol = true 119 | wrk := NewEmptyBackendWorker() 120 | wrk.UpdateSameGoroutine(cfg) 121 | return wrk 122 | } 123 | 124 | func NewEmptyBackendWorker() BackendWorker { 125 | return BackendWorker{ 126 | proxyCh: make(chan ProxyAction, 10), 127 | reqCh: make(chan BackendRequest, 5), 128 | connCheckCh: make(chan CheckOpenConns), 129 | updateCh: make(chan BackendConfig), 130 | closeCh: make(chan struct{}), 131 | } 132 | } 133 | 134 | type BackendWorker struct { 135 | activeConns int 136 | proxyCh chan ProxyAction 137 | reqCh chan BackendRequest 138 | connCheckCh chan CheckOpenConns 139 | updateCh chan BackendConfig 140 | closeCh chan struct{} 141 | 142 | Name string 143 | SendProxyProtocol bool 144 | OfflineStatusPacket mc.Packet 145 | DisconnectPacket mc.Packet 146 | 147 | HsModifier module.HandshakeModifier 148 | ConnCreator module.ConnectionCreator 149 | ConnLimiter module.ConnectionLimiter 150 | ServerState module.StateAgent 151 | StatusCache module.StatusCache 152 | } 153 | 154 | func (w *BackendWorker) Run() { 155 | go func(worker BackendWorker) { 156 | worker.Work() 157 | }(*w) 158 | } 159 | 160 | func (w *BackendWorker) Server() core.Server { 161 | return &BackendServer{ 162 | ch: w.ReqCh(), 163 | } 164 | } 165 | 166 | func (w *BackendWorker) ReqCh() chan<- BackendRequest { 167 | return w.reqCh 168 | } 169 | 170 | func (w *BackendWorker) HasActiveConn() bool { 171 | ch := make(chan bool) 172 | checker := CheckOpenConns{ 173 | Ch: ch, 174 | } 175 | w.connCheckCh <- checker 176 | ans := <-ch 177 | return ans 178 | } 179 | 180 | func (w *BackendWorker) Update(cfg BackendConfig) error { 181 | w.updateCh <- cfg 182 | return nil 183 | } 184 | 185 | func (w *BackendWorker) Close() { 186 | w.closeCh <- struct{}{} 187 | } 188 | 189 | //TODO: Need different name for this...? 190 | func (w *BackendWorker) UpdateSameGoroutine(wCfg BackendConfig) { 191 | if wCfg.Name != "" { 192 | playersConnected.Delete(prometheus.Labels{"host": w.Name}) 193 | w.Name = wCfg.Name 194 | playersConnected.WithLabelValues(w.Name).Add(float64(w.activeConns)) 195 | } 196 | if wCfg.UpdateProxyProtocol { 197 | w.SendProxyProtocol = wCfg.SendProxyProtocol 198 | } 199 | if len(wCfg.DisconnectPacket.Data) > 0 { 200 | w.DisconnectPacket = wCfg.DisconnectPacket 201 | } 202 | if len(wCfg.OfflineStatusPacket.Data) > 0 { 203 | w.OfflineStatusPacket = wCfg.OfflineStatusPacket 204 | } 205 | if wCfg.HsModifier != nil { 206 | w.HsModifier = wCfg.HsModifier 207 | } 208 | if wCfg.ConnCreator != nil { 209 | w.ConnCreator = wCfg.ConnCreator 210 | } 211 | if wCfg.ConnLimiter != nil { 212 | w.ConnLimiter = wCfg.ConnLimiter 213 | } 214 | if wCfg.ServerState != nil { 215 | w.ServerState = wCfg.ServerState 216 | } 217 | if wCfg.StatusCache != nil { 218 | w.StatusCache = wCfg.StatusCache 219 | } 220 | } 221 | 222 | func (wrk *BackendWorker) Work() { 223 | for { 224 | select { 225 | case req := <-wrk.reqCh: 226 | ans := wrk.HandleRequest(req) 227 | ans.ServerName = wrk.Name 228 | req.Ch <- ans 229 | case proxyAction := <-wrk.proxyCh: 230 | wrk.proxyRequest(proxyAction) 231 | case connCheck := <-wrk.connCheckCh: 232 | connCheck.Ch <- wrk.activeConns > 0 233 | case cfg := <-wrk.updateCh: 234 | wrk.UpdateSameGoroutine(cfg) 235 | case <-wrk.closeCh: 236 | close(wrk.reqCh) 237 | return 238 | } 239 | } 240 | } 241 | 242 | func (wrk *BackendWorker) proxyRequest(proxyAction ProxyAction) { 243 | switch proxyAction { 244 | case ProxyOpen: 245 | wrk.activeConns++ 246 | playersConnected.WithLabelValues(wrk.Name).Inc() 247 | case ProxyClose: 248 | wrk.activeConns-- 249 | playersConnected.WithLabelValues(wrk.Name).Dec() 250 | } 251 | } 252 | 253 | func (wrk *BackendWorker) HandleRequest(req BackendRequest) BackendAnswer { 254 | if wrk.ServerState != nil && wrk.ServerState.State() == core.Offline { 255 | switch req.ReqData.Type { 256 | case mc.Status: 257 | return NewStatusAnswer(wrk.OfflineStatusPacket) 258 | case mc.Login: 259 | return NewDisconnectAnswer(wrk.DisconnectPacket) 260 | } 261 | } 262 | 263 | if wrk.StatusCache != nil && req.ReqData.Type == mc.Status { 264 | ans, err := wrk.StatusCache.Status() 265 | if err != nil { 266 | return NewStatusAnswer(wrk.OfflineStatusPacket) 267 | } 268 | return NewStatusAnswer(ans) 269 | } 270 | 271 | if wrk.ConnLimiter != nil { 272 | if !wrk.ConnLimiter.Allow(req.ReqData) { 273 | return BackendAnswer{} 274 | } 275 | } 276 | connFunc := wrk.ConnCreator.Conn() 277 | if wrk.SendProxyProtocol { 278 | connFunc = func() (net.Conn, error) { 279 | addr := req.ReqData.Addr 280 | serverConn, err := wrk.ConnCreator.Conn()() 281 | if err != nil { 282 | return serverConn, err 283 | } 284 | header := &proxyproto.Header{ 285 | Version: 2, 286 | Command: proxyproto.PROXY, 287 | TransportProtocol: proxyproto.TCPv4, 288 | SourceAddr: addr, 289 | DestinationAddr: serverConn.RemoteAddr(), 290 | } 291 | _, err = header.WriteTo(serverConn) 292 | if err != nil { 293 | return serverConn, err 294 | } 295 | return serverConn, nil 296 | } 297 | } 298 | 299 | if wrk.HsModifier != nil { 300 | wrk.HsModifier.Modify(&req.ReqData.Handshake, req.ReqData.Addr.String()) 301 | } 302 | 303 | hsPk := req.ReqData.Handshake.Marshal() 304 | var secondPacket mc.Packet 305 | switch req.ReqData.Type { 306 | case mc.Status: 307 | secondPacket = mc.ServerBoundRequest{}.Marshal() 308 | case mc.Login: 309 | secondPacket = mc.ServerLoginStart{Name: mc.String(req.ReqData.Username)}.Marshal() 310 | } 311 | return NewProxyAnswer(hsPk, secondPacket, wrk.proxyCh, connFunc) 312 | } 313 | 314 | func NewBackendServer(ch chan<- BackendRequest) core.Server { 315 | return &BackendServer{ 316 | ch: ch, 317 | } 318 | } 319 | 320 | type BackendServer struct { 321 | ch chan<- BackendRequest 322 | } 323 | 324 | func (wrk *BackendServer) ConnAction(req core.RequestData) core.ServerAction { 325 | ans := wrk.HandleRequest(req) 326 | 327 | switch ans.Action() { 328 | case Disconnect: 329 | return core.DISCONNECT 330 | case Proxy: 331 | return core.PROXY 332 | case Close: 333 | return core.CLOSE 334 | case Error: 335 | return core.CLOSE 336 | default: 337 | return core.PROXY 338 | } 339 | } 340 | 341 | func (wrk *BackendServer) CreateConn(req core.RequestData) (c net.Conn, err error) { 342 | ans := wrk.HandleRequest(req) 343 | 344 | if ans.Action() != Proxy { 345 | return nil, core.ErrNoServerConn 346 | } 347 | 348 | return ans.serverConnFunc() 349 | } 350 | 351 | func (wrk *BackendServer) Status() mc.Packet { 352 | reqData := core.RequestData{ 353 | Type: mc.Status, 354 | } 355 | ans := wrk.HandleRequest(reqData) 356 | 357 | if ans.Action() != SendStatus { 358 | return mc.Packet{} 359 | } 360 | 361 | return ans.Response() 362 | } 363 | 364 | func (wrk *BackendServer) HandleRequest(req core.RequestData) BackendAnswer { 365 | backendReq := BackendRequest{ 366 | ReqData: core.RequestData{ 367 | Type: mc.Status, 368 | }, 369 | } 370 | ansCh := make(chan BackendAnswer) 371 | backendReq.Ch = ansCh 372 | wrk.ch <- backendReq 373 | return <-ansCh 374 | } 375 | -------------------------------------------------------------------------------- /worker/backend_manager.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "reflect" 7 | 8 | "github.com/realDragonium/Ultraviolet/config" 9 | ) 10 | 11 | var ErrSameConfig = errors.New("old and new config are the same") 12 | 13 | func NewBackendManager(manager WorkerManager, factory BackendFactoryFunc, cfgReader config.ServerConfigReader) (BackendManager, error) { 14 | bManager := BackendManager{ 15 | backends: make(map[string]Backend), 16 | domains: make(map[string]string), 17 | cfgs: make(map[string]config.ServerConfig), 18 | workerManager: manager, 19 | backendFactory: factory, 20 | configReader: cfgReader, 21 | } 22 | err := bManager.Update() 23 | return bManager, err 24 | } 25 | 26 | type BackendManager struct { 27 | backends map[string]Backend 28 | domains map[string]string 29 | cfgs map[string]config.ServerConfig 30 | 31 | backendFactory BackendFactoryFunc 32 | workerManager WorkerManager 33 | configReader config.ServerConfigReader 34 | } 35 | 36 | func (manager *BackendManager) Update() error { 37 | newCfgs, err := manager.configReader() 38 | if err != nil { 39 | return err 40 | } 41 | for _, newCfg := range newCfgs { 42 | _, err := config.ServerToBackendConfig(newCfg) 43 | if err != nil { 44 | return err 45 | } 46 | } 47 | 48 | //From here on forward its not possible anymore to get an error...? 49 | 50 | if len(manager.cfgs) == 0 { 51 | for _, cfg := range newCfgs { 52 | manager.addConfig(cfg) 53 | } 54 | return nil 55 | } 56 | manager.loadAllConfigs(newCfgs) 57 | log.Printf("Registered %v backend(s)", len(newCfgs)) 58 | return nil 59 | } 60 | 61 | // convert error should be checked before calling this method 62 | func (manager *BackendManager) addConfig(cfg config.ServerConfig) { 63 | manager.cfgs[cfg.ID()] = cfg 64 | for _, domain := range cfg.Domains { 65 | manager.domains[domain] = cfg.ID() 66 | } 67 | // Error has already been changed before calling 68 | bwCfg, _ := config.ServerToBackendConfig(cfg) 69 | backend := manager.backendFactory(bwCfg) 70 | manager.backends[cfg.ID()] = backend 71 | manager.workerManager.AddBackend(cfg.Domains, backend.Server()) 72 | } 73 | 74 | func (manager *BackendManager) removeConfig(cfg config.ServerConfig) { 75 | server, ok := manager.backends[cfg.ID()] 76 | log.Println(ok) 77 | if !ok { 78 | return 79 | } 80 | 81 | delete(manager.cfgs, cfg.ID()) 82 | for _, domain := range cfg.Domains { 83 | delete(manager.domains, domain) 84 | } 85 | manager.workerManager.RemoveBackend(cfg.Domains) 86 | 87 | //so a worker doesnt send a request to a closed backend 88 | server.Close() 89 | delete(manager.backends, cfg.ID()) 90 | } 91 | 92 | func (manager *BackendManager) updateConfig(cfg config.ServerConfig) { 93 | oldCfg := manager.cfgs[cfg.ID()] 94 | if reflect.DeepEqual(cfg, oldCfg) { 95 | return 96 | } 97 | 98 | domainStatus := make(map[string]int) 99 | for _, domain := range cfg.Domains { 100 | domainStatus[domain] += 1 101 | } 102 | 103 | for _, domain := range oldCfg.Domains { 104 | domainStatus[domain] += 2 105 | } 106 | 107 | removedDomains := []string{} 108 | addedDomains := []string{} 109 | for key, value := range domainStatus { 110 | switch value { 111 | case 1: // new 112 | manager.domains[key] = cfg.ID() 113 | addedDomains = append(addedDomains, key) 114 | case 2: // old 115 | removedDomains = append(removedDomains, key) 116 | delete(manager.domains, key) 117 | case 3: // both have it, so keep 118 | } 119 | } 120 | b := manager.backendByID(cfg.ID()) 121 | manager.workerManager.AddBackend(addedDomains, b.Server()) 122 | manager.workerManager.RemoveBackend(removedDomains) 123 | 124 | backendWorkerCfg, _ := config.ServerToBackendConfig(cfg) 125 | backendConfig := NewBackendConfig(backendWorkerCfg) 126 | b.Update(backendConfig) 127 | } 128 | 129 | func (manager *BackendManager) backendByID(id string) Backend { 130 | return manager.backends[id] 131 | } 132 | 133 | func (manager *BackendManager) loadAllConfigs(cfgs []config.ServerConfig) { 134 | newCfgs := make(map[string]config.ServerConfig) 135 | for _, cfg := range cfgs { 136 | newCfgs[cfg.ID()] = cfg 137 | } 138 | 139 | for id, oldCfg := range manager.cfgs { 140 | if _, ok := newCfgs[id]; !ok { 141 | manager.removeConfig(oldCfg) 142 | } 143 | } 144 | 145 | for id, newCfg := range newCfgs { 146 | if _, ok := manager.cfgs[id]; !ok { 147 | manager.addConfig(newCfg) 148 | continue 149 | } 150 | manager.updateConfig(newCfg) 151 | } 152 | } 153 | 154 | func (manager *BackendManager) CheckActiveConnections() bool { 155 | activeConns := false 156 | for _, bw := range manager.backends { 157 | answer := bw.HasActiveConn() 158 | if answer { 159 | activeConns = true 160 | break 161 | } 162 | } 163 | return activeConns 164 | } 165 | -------------------------------------------------------------------------------- /worker/backendaction_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=BackendAction"; DO NOT EDIT. 2 | 3 | package worker 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[Error-0] 12 | _ = x[Proxy-1] 13 | _ = x[Disconnect-2] 14 | _ = x[SendStatus-3] 15 | _ = x[Close-4] 16 | } 17 | 18 | const _BackendAction_name = "ErrorProxyDisconnectSendStatusClose" 19 | 20 | var _BackendAction_index = [...]uint8{0, 5, 10, 20, 30, 35} 21 | 22 | func (i BackendAction) String() string { 23 | if i >= BackendAction(len(_BackendAction_index)-1) { 24 | return "BackendAction(" + strconv.FormatInt(int64(i), 10) + ")" 25 | } 26 | return _BackendAction_name[_BackendAction_index[i]:_BackendAction_index[i+1]] 27 | } 28 | -------------------------------------------------------------------------------- /worker/proxyaction_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ProxyAction"; DO NOT EDIT. 2 | 3 | package worker 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ProxyOpen-0] 12 | _ = x[ProxyClose-1] 13 | } 14 | 15 | const _ProxyAction_name = "ProxyOpenProxyClose" 16 | 17 | var _ProxyAction_index = [...]uint8{0, 9, 19} 18 | 19 | func (i ProxyAction) String() string { 20 | if i < 0 || i >= ProxyAction(len(_ProxyAction_index)-1) { 21 | return "ProxyAction(" + strconv.FormatInt(int64(i), 10) + ")" 22 | } 23 | return _ProxyAction_name[_ProxyAction_index[i]:_ProxyAction_index[i+1]] 24 | } 25 | -------------------------------------------------------------------------------- /worker/run.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/prometheus/client_golang/prometheus/promhttp" 10 | "github.com/realDragonium/Ultraviolet/config" 11 | "github.com/realDragonium/Ultraviolet/core" 12 | ) 13 | 14 | var ( 15 | ReqCh chan net.Conn 16 | backendManager BackendManager 17 | ) 18 | 19 | func NewProxy(uvReader config.UVConfigReader, l net.Listener, cfgReader config.ServerConfigReader) core.Proxy { 20 | return &WorkerProxy{ 21 | uvReader: uvReader, 22 | listener: l, 23 | cfgReader: cfgReader, 24 | } 25 | } 26 | 27 | type WorkerProxy struct { 28 | uvReader config.UVConfigReader 29 | listener net.Listener 30 | cfgReader config.ServerConfigReader 31 | } 32 | 33 | func (p *WorkerProxy) Start() error { 34 | cfg, err := p.uvReader() 35 | if err != nil { 36 | return err 37 | } 38 | if ReqCh == nil { 39 | ReqCh = make(chan net.Conn, 50) 40 | } 41 | workerManager := NewWorkerManager(p.uvReader, ReqCh) 42 | workerManager.Start() 43 | backendManager, err = NewBackendManager(workerManager, BackendFactory, p.cfgReader) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | for i := 0; i < cfg.NumberOfListeners; i++ { 49 | go func(listener net.Listener, reqCh chan<- net.Conn) { 50 | serveListener(listener, reqCh) 51 | }(p.listener, ReqCh) 52 | } 53 | log.Printf("Running %v listener(s)", cfg.NumberOfListeners) 54 | 55 | if cfg.UsePrometheus { 56 | log.Println("Starting prometheus...") 57 | mux := http.NewServeMux() 58 | mux.Handle("/metrics", promhttp.Handler()) 59 | promeServer := &http.Server{Addr: cfg.PrometheusBind, Handler: mux} 60 | go func() { 61 | log.Println(promeServer.ListenAndServe()) 62 | }() 63 | } 64 | 65 | log.Println("Now starting api endpoint") 66 | UsedAPI := NewAPI(backendManager) 67 | go UsedAPI.Run(cfg.APIBind) 68 | log.Println("Finished starting up") 69 | 70 | return nil 71 | } 72 | 73 | func serveListener(listener net.Listener, reqCh chan<- net.Conn) { 74 | for { 75 | conn, err := listener.Accept() 76 | if err != nil { 77 | if errors.Is(err, net.ErrClosed) { 78 | log.Printf("net.Listener was closed, stopping with accepting calls") 79 | break 80 | } 81 | log.Println(err) 82 | continue 83 | } 84 | reqCh <- conn 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /worker/type.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/realDragonium/Ultraviolet/core" 7 | "github.com/realDragonium/Ultraviolet/mc" 8 | ) 9 | 10 | //go:generate stringer -type=BackendAction 11 | type BackendAction byte 12 | 13 | const ( 14 | Error BackendAction = iota 15 | Proxy 16 | Disconnect 17 | SendStatus 18 | Close 19 | ) 20 | 21 | //go:generate stringer -type=ProxyAction 22 | type ProxyAction int8 23 | 24 | const ( 25 | ProxyOpen ProxyAction = iota 26 | ProxyClose 27 | ) 28 | 29 | type BackendRequest struct { 30 | ReqData core.RequestData 31 | Ch chan<- BackendAnswer 32 | } 33 | 34 | type BackendAnswer struct { 35 | ServerName string 36 | serverConnFunc func() (net.Conn, error) 37 | action BackendAction 38 | proxyCh chan ProxyAction 39 | 40 | firstPacket mc.Packet 41 | secondPacket mc.Packet 42 | } 43 | 44 | func NewDisconnectAnswer(p mc.Packet) BackendAnswer { 45 | return BackendAnswer{ 46 | action: Disconnect, 47 | firstPacket: p, 48 | } 49 | } 50 | 51 | func NewStatusAnswer(p mc.Packet) BackendAnswer { 52 | return BackendAnswer{ 53 | action: SendStatus, 54 | firstPacket: p, 55 | } 56 | } 57 | 58 | func NewProxyAnswer(p1, p2 mc.Packet, proxyCh chan ProxyAction, connFunc func() (net.Conn, error)) BackendAnswer { 59 | return BackendAnswer{ 60 | action: Proxy, 61 | serverConnFunc: connFunc, 62 | firstPacket: p1, 63 | secondPacket: p2, 64 | proxyCh: proxyCh, 65 | } 66 | } 67 | 68 | func NewCloseAnswer() BackendAnswer { 69 | return BackendAnswer{ 70 | action: Close, 71 | } 72 | } 73 | 74 | func (ans BackendAnswer) ServerConn() (net.Conn, error) { 75 | return ans.serverConnFunc() 76 | } 77 | func (ans BackendAnswer) Response() mc.Packet { 78 | return ans.firstPacket 79 | } 80 | func (ans BackendAnswer) Response2() mc.Packet { 81 | return ans.secondPacket 82 | } 83 | func (ans BackendAnswer) ProxyCh() chan ProxyAction { 84 | return ans.proxyCh 85 | } 86 | func (ans BackendAnswer) Action() BackendAction { 87 | return ans.action 88 | } 89 | -------------------------------------------------------------------------------- /worker/worker.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "log" 8 | "net" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "github.com/prometheus/client_golang/prometheus" 14 | "github.com/prometheus/client_golang/prometheus/promauto" 15 | ultraviolet "github.com/realDragonium/Ultraviolet" 16 | "github.com/realDragonium/Ultraviolet/config" 17 | "github.com/realDragonium/Ultraviolet/core" 18 | "github.com/realDragonium/Ultraviolet/mc" 19 | ) 20 | 21 | const ( 22 | maxHandshakeLength int = 264 // 264 -> 'max handshake packet length' + 1 23 | // packetLength:2 + packet ID: 1 + protocol version:2 + max string length:255 + port:2 + state: 1 -> 2+1+2+255+2+1 = 263 24 | ) 25 | 26 | type UpdatableWorker interface { 27 | Update(data core.ServerCatalog) 28 | } 29 | 30 | var ( 31 | unknownServerAddr = "unknown" 32 | 33 | requestBuckets = []float64{.0001, .0005, .001, .005, .01, .05, .1, .5, 1, 5} 34 | processRequests = promauto.NewHistogramVec(prometheus.HistogramOpts{ 35 | Namespace: "ultraviolet", 36 | Name: "request_duration_seconds", 37 | Help: "Histogram request processing durations.", 38 | Buckets: requestBuckets, 39 | }, []string{"action", "server", "type"}) 40 | ) 41 | 42 | func NewWorker(cfg config.WorkerConfig, reqCh <-chan net.Conn) BasicWorker { 43 | defaultStatusPk := cfg.DefaultStatus.Marshal() 44 | closeAnswer := NewCloseAnswer() 45 | closeAnswer.ServerName = unknownServerAddr 46 | dict := core.NewEmptyServerCatalog(defaultStatusPk, mc.Packet{}) 47 | return BasicWorker{ 48 | reqCh: reqCh, 49 | closeAnswer: closeAnswer, 50 | serverDict: dict, 51 | ioTimeout: cfg.IOTimeout, 52 | closeCh: make(chan struct{}), 53 | updateCh: make(chan core.ServerCatalog), 54 | } 55 | } 56 | 57 | type BasicWorker struct { 58 | reqCh <-chan net.Conn 59 | closeCh chan struct{} 60 | updateCh chan core.ServerCatalog 61 | 62 | closeAnswer BackendAnswer 63 | 64 | ioTimeout time.Duration 65 | serverDict core.ServerCatalog 66 | } 67 | 68 | func (w *BasicWorker) IODeadline() time.Time { 69 | return time.Now().Add(w.ioTimeout) 70 | } 71 | 72 | func (w *BasicWorker) CloseCh() chan<- struct{} { 73 | return w.closeCh 74 | } 75 | 76 | func (w *BasicWorker) Update(data core.ServerCatalog) { 77 | w.updateCh <- data 78 | } 79 | 80 | func (w *BasicWorker) SetServers(servers core.ServerCatalog) { 81 | w.serverDict = servers 82 | } 83 | 84 | func (bw *BasicWorker) Work() { 85 | for { 86 | select { 87 | case conn := <-bw.reqCh: 88 | if err := bw.ProcessConnection(conn); err != nil { 89 | if errors.Is(err, core.ErrClientToSlow) { 90 | log.Printf("client %v was to slow with sending packet to us", conn.RemoteAddr()) 91 | } else { 92 | log.Printf("error while trying to read: %v", err) 93 | } 94 | } 95 | case <-bw.closeCh: 96 | return 97 | case serverChs := <-bw.updateCh: 98 | bw.SetServers(serverChs) 99 | } 100 | } 101 | } 102 | 103 | func (bw *BasicWorker) NotSafeYet_ProcessConnection(conn net.Conn) (core.RequestData, error) { 104 | // TODO: When handshake gets too long stuff goes wrong, prevent is from crashing when that happens 105 | b := bufio.NewReaderSize(conn, maxHandshakeLength) 106 | handshake, err := mc.ReadPacket3_Handshake(b) 107 | if err != nil { 108 | log.Printf("error parsing handshake from %v - error: %v", conn.RemoteAddr(), err) 109 | } 110 | t := mc.RequestState(handshake.NextState) 111 | if t == mc.UnknownState { 112 | return core.RequestData{}, core.ErrNotValidHandshake 113 | } 114 | request := core.RequestData{ 115 | Type: t, 116 | ServerAddr: handshake.ParseServerAddress(), 117 | Addr: conn.RemoteAddr(), 118 | Handshake: handshake, 119 | } 120 | 121 | packet, _ := mc.ReadPacket3(b) 122 | if t == mc.Login { 123 | loginStart, _ := mc.UnmarshalServerBoundLoginStart(packet) 124 | request.Username = string(loginStart.Name) 125 | } 126 | return request, nil 127 | } 128 | 129 | func (bw *BasicWorker) ProcessConnection(conn net.Conn) (err error) { 130 | req, err := bw.ReadConnection(conn) 131 | if err != nil { 132 | return 133 | } 134 | 135 | server, err := bw.serverDict.Find(req.ServerAddr) 136 | if err != nil { 137 | return 138 | } 139 | return ultraviolet.ProcessServer(conn, server, req) 140 | } 141 | 142 | // TODO: 143 | // - Adding some more error tests 144 | func (bw *BasicWorker) ReadConnection(conn net.Conn) (reqData core.RequestData, err error) { 145 | mcConn := mc.NewMcConn(conn) 146 | conn.SetDeadline(bw.IODeadline()) 147 | 148 | handshakePacket, err := mcConn.ReadPacket() 149 | if errors.Is(err, os.ErrDeadlineExceeded) { 150 | return reqData, core.ErrClientToSlow 151 | } else if err != nil { 152 | return 153 | } 154 | 155 | handshake, err := mc.UnmarshalServerBoundHandshake(handshakePacket) 156 | if err != nil { 157 | log.Printf("error while parsing handshake: %v", err) 158 | } 159 | reqType := mc.RequestState(handshake.NextState) 160 | if reqType == mc.UnknownState { 161 | return reqData, core.ErrNotValidHandshake 162 | } 163 | 164 | packet, err := mcConn.ReadPacket() 165 | if errors.Is(err, os.ErrDeadlineExceeded) { 166 | return reqData, core.ErrClientToSlow 167 | } else if err != nil { 168 | return 169 | } 170 | conn.SetDeadline(time.Time{}) 171 | 172 | serverAddr := strings.ToLower(handshake.ParseServerAddress()) 173 | reqData = core.RequestData{ 174 | Type: reqType, 175 | ServerAddr: serverAddr, 176 | Addr: conn.RemoteAddr(), 177 | Handshake: handshake, 178 | } 179 | 180 | if reqType == mc.Login { 181 | loginStart, err := mc.UnmarshalServerBoundLoginStart(packet) 182 | if err != nil { 183 | log.Printf("error while parsing login packet: %v", err) 184 | return reqData, err 185 | } 186 | reqData.Username = string(loginStart.Name) 187 | } 188 | 189 | return reqData, nil 190 | } 191 | 192 | func (bw *BasicWorker) ProcessAnswer(conn net.Conn, ans BackendAnswer) { 193 | clientMcConn := mc.NewMcConn(conn) 194 | switch ans.Action() { 195 | case Proxy: 196 | sConn, err := ans.ServerConn() 197 | if err != nil { 198 | log.Printf("Err when creating server connection: %v", err) 199 | conn.Close() 200 | return 201 | } 202 | mcServerConn := mc.NewMcConn(sConn) 203 | mcServerConn.WritePacket(ans.Response()) 204 | mcServerConn.WritePacket(ans.Response2()) 205 | go func(client, serverConn net.Conn, proxyCh chan ProxyAction) { 206 | proxyCh <- ProxyOpen 207 | ProxyConnection(client, serverConn) 208 | proxyCh <- ProxyClose 209 | }(conn, sConn, ans.ProxyCh()) 210 | case Disconnect: 211 | clientMcConn.WritePacket(ans.Response()) 212 | conn.Close() 213 | case SendStatus: 214 | clientMcConn.WritePacket(ans.Response()) 215 | conn.SetDeadline(bw.IODeadline()) 216 | pingPacket, err := clientMcConn.ReadPacket() 217 | if err != nil { 218 | conn.Close() 219 | return 220 | } 221 | clientMcConn.WritePacket(pingPacket) 222 | conn.Close() 223 | case Close: 224 | conn.Close() 225 | } 226 | 227 | } 228 | 229 | func Proxy_IOCopy(client, server net.Conn) { 230 | // Close behavior doesnt seem to work that well 231 | go func() { 232 | io.Copy(server, client) 233 | client.Close() 234 | }() 235 | io.Copy(client, server) 236 | server.Close() 237 | } 238 | 239 | // TODO: 240 | // - check or servers close the connection when they disconnect players if not add something to prevent abuse 241 | func ProxyConnection(client, server net.Conn) { 242 | go func() { 243 | pipe(server, client) 244 | client.Close() 245 | }() 246 | pipe(client, server) 247 | server.Close() 248 | } 249 | 250 | func pipe(c1, c2 net.Conn) { 251 | buffer := make([]byte, 0xffff) 252 | for { 253 | n, err := c1.Read(buffer) 254 | if err != nil { 255 | return 256 | } 257 | _, err = c2.Write(buffer[:n]) 258 | if err != nil { 259 | return 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /worker/worker_manager.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "log" 5 | "net" 6 | 7 | "github.com/realDragonium/Ultraviolet/config" 8 | "github.com/realDragonium/Ultraviolet/core" 9 | "github.com/realDragonium/Ultraviolet/mc" 10 | ) 11 | 12 | type WorkerManager interface { 13 | AddBackend(domains []string, server core.Server) 14 | RemoveBackend(domains []string) 15 | KnowsDomain(domain string) bool 16 | Register(worker UpdatableWorker, update bool) 17 | Start() error 18 | } 19 | 20 | func NewWorkerManager(cfg config.UVConfigReader, reqCh <-chan net.Conn) WorkerManager { 21 | manager := workerManager{ 22 | reqCh: reqCh, 23 | cfgReader: cfg, 24 | domains: core.NewEmptyServerCatalog(mc.Packet{}, mc.Packet{}), 25 | workers: []UpdatableWorker{}, 26 | } 27 | return &manager 28 | } 29 | 30 | type workerManager struct { 31 | cfgReader config.UVConfigReader 32 | reqCh <-chan net.Conn 33 | domains core.BasicServerCatalog 34 | workers []UpdatableWorker 35 | } 36 | 37 | func (manager *workerManager) Start() error { 38 | cfg, err := manager.cfgReader() 39 | if err != nil { 40 | return err 41 | } 42 | workerCfg := config.NewWorkerConfig(cfg) 43 | for i := 0; i < cfg.NumberOfWorkers; i++ { 44 | wrk := NewWorker(workerCfg, manager.reqCh) 45 | go func(bw BasicWorker) { 46 | bw.Work() 47 | }(wrk) 48 | manager.Register(&wrk, true) 49 | } 50 | log.Printf("Running %v worker(s)", cfg.NumberOfWorkers) 51 | return nil 52 | } 53 | 54 | func (manager *workerManager) SetReqChan(reqCh <-chan net.Conn) { 55 | manager.reqCh = reqCh 56 | } 57 | 58 | func (manager *workerManager) AddBackend(domains []string, server core.Server) { 59 | for _, domain := range domains { 60 | manager.domains.ServerDict[domain] = server 61 | } 62 | manager.update() 63 | } 64 | 65 | func (manager *workerManager) RemoveBackend(domains []string) { 66 | for _, domain := range domains { 67 | delete(manager.domains.ServerDict, domain) 68 | } 69 | manager.update() 70 | } 71 | 72 | func (manager *workerManager) Register(worker UpdatableWorker, update bool) { 73 | manager.workers = append(manager.workers, worker) 74 | if update { 75 | worker.Update(manager.domains) 76 | } 77 | } 78 | 79 | func (manager *workerManager) update() { 80 | for _, wrk := range manager.workers { 81 | wrk.Update(manager.domains) 82 | } 83 | } 84 | 85 | func (manager *workerManager) KnowsDomain(domain string) bool { 86 | _, err := manager.domains.Find(domain) 87 | return err == nil 88 | } 89 | -------------------------------------------------------------------------------- /worker/worker_manager_test.go: -------------------------------------------------------------------------------- 1 | package worker_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/realDragonium/Ultraviolet/config" 8 | "github.com/realDragonium/Ultraviolet/core" 9 | "github.com/realDragonium/Ultraviolet/mc" 10 | "github.com/realDragonium/Ultraviolet/worker" 11 | ) 12 | 13 | type testUpdatableWorkerCounter struct { 14 | updatesReceived int 15 | } 16 | 17 | func (worker *testUpdatableWorkerCounter) Update(data core.ServerCatalog) { 18 | worker.updatesReceived++ 19 | } 20 | 21 | type testServer struct{} 22 | 23 | func (s testServer) ConnAction(req core.RequestData) core.ServerAction { 24 | return core.CLOSE 25 | } 26 | 27 | func (s testServer) CreateConn(req core.RequestData) (c net.Conn, err error) { 28 | return nil, nil 29 | } 30 | 31 | func (s testServer) Status() mc.Packet { 32 | return mc.Packet{} 33 | } 34 | 35 | func TestRegisterServerConfig(t *testing.T) { 36 | cfg := config.UltravioletConfig{} 37 | t.Run("add backend", func(t *testing.T) { 38 | manager := worker.NewWorkerManager(config.NewUVReader(cfg), nil) 39 | domains := []string{"uv", "uv2"} 40 | manager.AddBackend(domains, testServer{}) 41 | for _, domain := range domains { 42 | if !manager.KnowsDomain(domain) { 43 | t.Error("manager should have known this domain") 44 | } 45 | } 46 | }) 47 | 48 | t.Run("remove backend", func(t *testing.T) { 49 | manager := worker.NewWorkerManager(config.NewUVReader(cfg), nil) 50 | domain := "uv2" 51 | domains := []string{"uv", domain} 52 | manager.AddBackend(domains, testServer{}) 53 | 54 | removeDomains := []string{domain} 55 | manager.RemoveBackend(removeDomains) 56 | 57 | if manager.KnowsDomain(domain) { 58 | t.Error("manager should NOT have known this domain") 59 | } 60 | }) 61 | 62 | t.Run("updates workers when registering", func(t *testing.T) { 63 | manager := worker.NewWorkerManager(config.NewUVReader(cfg), nil) 64 | wrk := testUpdatableWorkerCounter{} 65 | manager.Register(&wrk, true) 66 | 67 | if wrk.updatesReceived != 1 { 68 | t.Fatal("expected to receive an update") 69 | } 70 | }) 71 | 72 | t.Run("doesnt update workers when registering", func(t *testing.T) { 73 | manager := worker.NewWorkerManager(config.NewUVReader(cfg), nil) 74 | wrk := testUpdatableWorkerCounter{} 75 | manager.Register(&wrk, false) 76 | 77 | if wrk.updatesReceived != 0 { 78 | t.Fatal("should NOT have received an update") 79 | } 80 | }) 81 | 82 | t.Run("does updates when adding backend", func(t *testing.T) { 83 | manager := worker.NewWorkerManager(config.NewUVReader(cfg), nil) 84 | wrk := testUpdatableWorkerCounter{} 85 | manager.Register(&wrk, false) 86 | 87 | domain := "uv2" 88 | domains := []string{"uv", domain} 89 | manager.AddBackend(domains, testServer{}) 90 | 91 | if wrk.updatesReceived != 1 { 92 | t.Fatal("expected to receive an update") 93 | } 94 | }) 95 | 96 | t.Run("does updates when removing backend", func(t *testing.T) { 97 | manager := worker.NewWorkerManager(config.NewUVReader(cfg), nil) 98 | wrk := testUpdatableWorkerCounter{} 99 | manager.Register(&wrk, false) 100 | 101 | domain := "uv2" 102 | domains := []string{"uv", domain} 103 | manager.RemoveBackend(domains) 104 | if wrk.updatesReceived != 1 { 105 | t.Fatal("expected to receive an update") 106 | } 107 | }) 108 | 109 | } 110 | --------------------------------------------------------------------------------