Skip to content

Commit 56019a5

Browse files
committed
init: initial commit
0 parents  commit 56019a5

File tree

10 files changed

+1528
-0
lines changed

10 files changed

+1528
-0
lines changed

Diff for: .gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
config.json
2+
*.bak
3+
*.drawio*
4+
bin/

Diff for: LICENSE

+674
Large diffs are not rendered by default.

Diff for: README.md

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# hlmux
2+
3+
Multiplexer for Half-Life games (and its mods)
4+
5+
<div align="center">
6+
7+
![hlmux logo](./assets/logo.png)
8+
9+
</div>
10+
11+
*hlmux* is a gateway of multiple upstream GoldSource servers, offering a mechanism to redirect a client among these upstreams.
12+
13+
⚠ THIS PROJECT IS STILL UNDER CONSTRUCTION.
14+
15+
## Usage
16+
17+
*hlmux* is a Go package, and theoretically it's just a building block for a gateway application.
18+
19+
### hlmuxd
20+
21+
For convenience, we offer an official gateway application [*hlmuxd*](./cmd/hlmuxd) to ease most users' pains. It also serves as an example to demonstrate the usage of *hlmux*.
22+
23+
* Compilation
24+
25+
```
26+
mkdir -p ./bin
27+
go build -o ./bin ./cmd/hlmuxd
28+
```
29+
30+
* Sample configuration (`config.json`)
31+
32+
```json
33+
{
34+
"bind": "0.0.0.0:27015",
35+
"api": "0.0.0.0:27081",
36+
"ttl": "30",
37+
"upstreams": [
38+
{
39+
"name": "cs1",
40+
"address": "10.1.80.51:27001",
41+
"default": true
42+
},
43+
{
44+
"name": "cs2",
45+
"address": "10.1.80.51:27002"
46+
}
47+
]
48+
}
49+
```
50+
51+
* Running
52+
53+
```
54+
./bin/hlmuxd -config config.json
55+
56+
# or without compilation
57+
go run ./cmd/hlmuxd -config config.json
58+
```
59+
60+
## How does it work?
61+
62+
Valve has banned `connect` command for a long time ([related issue][redirect-issue])
63+
as a part of [admin slowhacking][admin-slowhacking] mitigation. Many redirection plugins does not work any more, while they do serve
64+
as practical components, especially in community servers.
65+
66+
This project implements a gateway in front of multiple GoldSource game servers, so that the
67+
gateway can have its own decision on the traffic forwarding.
68+
After in-game plugins tell the gateway which nexthop the clients want, their traffic will be forwarded to the target server when they send handshakes next time.
69+
70+
The client side command `retry` is able to invoke re-handshaking (which is different from `reconnect`). Unfortunately, we have known that Valve's [page][admin-slowhacking] is out of date by inspecting `hw.so` (or `hw.dll` on Windows) as shown below: `retry` is also banned, regardless of the value of `cl_filterstuffcmd`.
71+
72+
![stuff text filter in hw.so](./assets/ghidra-hw.png)
73+
74+
Good news is that there is still an approach to trick the clients into a state similar to a manually-issued `retry`.
75+
76+
[admin-slowhacking]: https://developer.valvesoftware.com/wiki/Admin_Slowhacking
77+
[redirect-issue]: https://github.com/ValveSoftware/halflife/issues/5

Diff for: assets/ghidra-hw.png

126 KB
Loading

Diff for: assets/logo.png

4.03 KB
Loading

Diff for: cmd/hlmuxd/hlmuxd.go

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// HLMUX
2+
//
3+
// Copyright (C) 2022 hlpkg-dev
4+
//
5+
// This program is free software: you can redistribute it and/or modify it under
6+
// the terms of the GNU General Public License as published by the Free Software
7+
// Foundation, either version 3 of the License, or (at your option) any later
8+
// version.
9+
//
10+
// This program is distributed in the hope that it will be useful, but WITHOUT
11+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12+
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13+
// details.
14+
//
15+
// You should have received a copy of the GNU General Public License along with
16+
// this program. If not, see <https://www.gnu.org/licenses/>.
17+
18+
package main
19+
20+
import (
21+
"bufio"
22+
"encoding/json"
23+
"flag"
24+
"fmt"
25+
"log"
26+
"net"
27+
"net/http"
28+
"os"
29+
"strings"
30+
31+
"github.com/hlpkg-dev/hlmux"
32+
)
33+
34+
type Upstream struct {
35+
Name string `json:"name"`
36+
Default bool `json:"default"`
37+
Address string `json:"address"`
38+
}
39+
40+
type Config struct {
41+
Bind string `json:"bind"`
42+
API string `json:"api"`
43+
TTL int `json:"ttl"`
44+
Upstreams []*Upstream `json:"upstreams"`
45+
}
46+
47+
type Set struct {
48+
Proxy string `json:"proxy"`
49+
Upstream string `json:"upstream"`
50+
}
51+
52+
var flagConfig = flag.String("config", "config.json", "Configuration file")
53+
54+
func readConfig() (*Config, error) {
55+
var config Config
56+
57+
data, err := os.ReadFile(*flagConfig)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
if json.Unmarshal(data, &config); err != nil {
63+
return nil, err
64+
}
65+
66+
return &config, nil
67+
}
68+
69+
func main() {
70+
config, err := readConfig()
71+
if err != nil {
72+
log.Fatalf("cannot read config: %v", err)
73+
}
74+
75+
if len(config.Upstreams) == 0 {
76+
log.Fatalf("cannot find valid upstreams")
77+
}
78+
79+
upstreams := make(map[string]*net.UDPAddr)
80+
var defaultUpstream *net.UDPAddr
81+
for _, upstream := range config.Upstreams {
82+
addr, err := net.ResolveUDPAddr("udp", upstream.Address)
83+
if err != nil {
84+
log.Fatalf("cannot resolve \"%s\": %v", upstream.Address, err)
85+
}
86+
upstreams[upstream.Name] = addr
87+
88+
if upstream.Default {
89+
defaultUpstream = addr
90+
}
91+
}
92+
93+
if defaultUpstream == nil {
94+
defaultUpstream = upstreams[config.Upstreams[0].Name]
95+
}
96+
97+
log.Printf("default upstream: %v", defaultUpstream)
98+
99+
mux := hlmux.NewMux(defaultUpstream)
100+
go func() {
101+
reader := bufio.NewReader(os.Stdin)
102+
for {
103+
fmt.Print("> ")
104+
line, err := reader.ReadString('\n')
105+
if err != nil {
106+
return
107+
}
108+
tokens := strings.FieldsFunc(line, func(c rune) bool {
109+
return c == ' ' || c == '\n'
110+
})
111+
if len(tokens) > 0 {
112+
switch tokens[0] {
113+
case "ls":
114+
conns := mux.Connections()
115+
if len(conns) == 0 {
116+
fmt.Println("no connections")
117+
}
118+
for i, conn := range conns {
119+
fmt.Printf(`
120+
[%d] Client: %v Proxy: %v Upstream: %v Next: %v
121+
`, i, conn.Client(), conn.Proxy(), conn.Upstream(), conn.NextUpstream())
122+
}
123+
case "update":
124+
clientAddr, err := net.ResolveUDPAddr("udp", tokens[1])
125+
if err != nil {
126+
fmt.Printf("invalid client: %v\n", err)
127+
}
128+
if conn := mux.FindConnectionByClient(clientAddr); conn != nil {
129+
addr, err := net.ResolveUDPAddr("udp", tokens[2])
130+
if err != nil {
131+
fmt.Printf("invalid upstream: %v\n", err)
132+
} else {
133+
conn.SetNextUpstream(addr)
134+
}
135+
} else {
136+
fmt.Printf("connection not found")
137+
}
138+
}
139+
}
140+
}
141+
}()
142+
go func() {
143+
// recommended to use more powerful libraries like `gin`
144+
145+
http.HandleFunc("/api/v1/set", func(w http.ResponseWriter, r *http.Request) {
146+
if r.Method != "POST" {
147+
w.WriteHeader(http.StatusNotImplemented)
148+
return
149+
}
150+
var s Set
151+
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
152+
w.WriteHeader(http.StatusBadRequest)
153+
return
154+
}
155+
156+
if upstream, ok := upstreams[s.Upstream]; ok {
157+
found := false
158+
for _, conn := range mux.Connections() {
159+
if conn.Proxy().String() == s.Proxy {
160+
found = true
161+
log.Printf("set client %v next upstream to %v", conn.Client(), upstream)
162+
conn.SetNextUpstream(upstream)
163+
break
164+
}
165+
}
166+
167+
if found {
168+
w.WriteHeader(http.StatusOK)
169+
} else {
170+
w.WriteHeader(http.StatusNotFound)
171+
}
172+
} else {
173+
w.WriteHeader(http.StatusNotFound)
174+
}
175+
})
176+
177+
if err := http.ListenAndServe(config.API, nil); err != nil {
178+
log.Printf("cannot server http: %v", err)
179+
}
180+
}()
181+
if err := mux.Run(config.Bind); err != nil {
182+
log.Fatalf("cannot init mux: %v", err)
183+
}
184+
}

0 commit comments

Comments
 (0)