Skip to content

Commit bc4520c

Browse files
committed
feature: Add a sample client for the remote control
1 parent 3772c44 commit bc4520c

File tree

5 files changed

+305
-0
lines changed

5 files changed

+305
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ There are some reference clients:
2727
* [ssl-ref-client](./cmd/ssl-ref-client): A client that receives referee messages
2828
* [ssl-auto-ref-client](./cmd/ssl-auto-ref-client/README.md): A client that connects to the controller as an autoRef
2929
* [ssl-team-client](./cmd/ssl-team-client/README.md): A client that connects to the controller as a team
30+
* [ssl-remote-control-client](./cmd/ssl-remote-control-client/README.md): A client that connects to the controller as a remote-control
3031
* [ssl-ci-test-client](./cmd/ssl-ci-test-client/README.md): A client that connects to the CI interface of the controller
3132

3233
### Comparison to ssl-refbox
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# ssl-remote-control-client
2+
3+
This folder contains a sample client that connects to the remote-control interface of the game-controller.
4+
5+
## Protocol
6+
The communication is established with a bidirectional TCP connection. Messages are encoded with [Protocol Buffers](https://developers.google.com/protocol-buffers/). Each message is preceded by an uvarint containing the message size in bytes, see https://cwiki.apache.org/confluence/display/GEODE/Delimiting+Protobuf+Messages for details.
7+
8+
The .proto files can be found in [../../proto](../../proto).
9+
10+
The default port is `10011` for plain connections and 10111 for TLS encrypted connections. The IP to connect to can be determined using the multicast referee messages.
11+
12+
## Connection sequence
13+
The connection is described in the following sequence diagram:
14+
15+
![sequence diagram](https://www.websequencediagrams.com/cgi-bin/cdraw?lz=IyBodHRwczovL3d3dy53ZWJzZXF1ZW5jZWRpYWdyYW1zLmNvbS8KClJlbW90ZUNvbnRyb2wtPgACB2xlcjogZXN0YWJsaXNoIFRDUCBjb25uZWN0aW9uCgAbCgAjDmdlbmVyYXRlIG5ldyB0b2tlAB0OAF4NOiAAYQpSZXBseSAoAC0GICkAeBwAgSINUmVnaXN0cmF0aW9uICggdGVhbSwgWwB0Biwgc2lnbmF0dXJlIF0gKQCBKAwAgVgOdmVyaWZ5AIEdEgARFQBLCQCBKy5vayB8IHJlamVjdCApCgpsb29wAIE8KVRvAIMKCgA6PGVuZAo&s=napkin)
16+
17+
Source to generate the diagram: [communication_remote-control.txt](./communication_remote-control.txt)
18+
19+
## Connection stability
20+
Clients should deal with connection losts (reconnect). The game-controller may be restarted due to various reasons like crashes or other technical issues.
21+
22+
## Secure connection
23+
The connection can optionally be secured by signing each request using a RSA key.
24+
25+
The private key is used on the client side to sign the complete message, excluding the signature itself.
26+
The public key must be provided to the game-controller.
27+
By default, the game-controller searches for public keys in [config/trusted_keys/team](../../config/trusted_keys/team) with the pattern `<teamName>.pub.pem`. The team name is case-sensitive and must be equal to one of the team names that are send via the referee protocol (including spaces, etc.). Each team can only connect once.
28+
29+
The [genKey.sh](../../tools/genKey.sh) script can be used to generate a new pair of public and private key.
30+
31+
The controller sends a token with each reply. It must be included in the next request, when using the signature. The token is required to avoid replay attacks.
32+
33+
If a public key is present for the team provided during registration, a signature is required. Else, the signature is ignored. The controller reply indicates, if the last request could be verified.
34+
35+
## Sample client
36+
The sample client, that is included in this folder, can be used to test the connection. It can be run with
37+
```bash
38+
go run cmd/ssl-remote-control-client/main.go
39+
```
40+
Pass it the `-h` parameter to get the available options.
41+
42+
By default, it tries to connect as "YELLOW".
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEowIBAAKCAQEApogmLRlwxd8hrU8h3zSLRzvMZVKKPi1EaVDTU5n6utSus1H6
3+
FqyBEykQbJm3dQ2X6nrnH2z3bimIBEeKAfeFNMBTq1CpNzCDmQFE1snBj3fKdRXz
4+
mKCvwZPGO+qKCNzHi+vhCtXTTZX/wbMRkRjrOq24gAiHxmFczLa94MpzeD3xLpq+
5+
7P01DrsyVvHktiIcTHs+1OT4VLMJvtr9slbCeWcXgMFvfRME201FcaOjmCg9jrKH
6+
HjQF0MHnd3hhsSOARoffQ/X/Lr1xOA/ixPEo5hxEGiRuUAPrn7iZWSpH6iRWbJWe
7+
2ZH+oj+WgEc7BR2F7odQ91irRrSxNsVs+I55WwIDAQABAoIBAGggZU55w8vVkucc
8+
vZ8k6ZlmyIzqKUprX4VCZoC1nNLJPVsefPNEdYiXeo+NJerozv5sTquVpLia+1NB
9+
sAc+z2mGgEp0Kvo5OW+oHXT3vjGIw2ymhyP+BSdS0PaR1jFoatUQbiwqOu8eRUbG
10+
Qsuo+xw7l0tnCg5+vlm6QcuWitC5V9H/t5AViXOfFaPBj9acHuWpszSiH5RBmZXi
11+
MBfwQiXtVDGentHca2B4tcNjSc89Wuv1DK3Y1v1/s7HukDFTgE8UdMFgaLRzt1N4
12+
OQUBhi15ZBFb2xKIZzRFoA9BWlksQ6BBPkVLt5Ka3Rfb34MDmAuroVdc1LLmCKeC
13+
6yQsUuECgYEA2WDk/T0K+GVBOfIciytkYx5/qNsvzC2bQ1E0X1x9VV2B0e1luKPt
14+
WOwet5UjVXnkjFXmLORQR0MUxLn97e/3+pWBjFUlhYQl52z6GRmTumscR3VbkJnv
15+
mrqAP8mzbtv7b4a5R8BD1OtgzeFRVyIb8jrLFBO8mXyWzyUxEAb7CbkCgYEAxB6T
16+
zJvV8JaU1o2yB8Oqlbi30pvz/lhwVlF9WWozX6a2fdhPdFMPktb6vOpARpsQlrZ7
17+
DydNAV83p6WDIpQUSOO8Me+YH+AdpKldgNGYQsS4YjtbP8DT8ihl3YJpQZ5a5mUW
18+
PItuJLoduGxtOqsGGxjhYLExd0i7REdCNSROlbMCgYEAvy2uduHG0isLMJE0dVlW
19+
Uq4yDCmpYeMCWDQE4ZGQURGJ6TzmZ3sUdU5EvaSWjMhFLv8lDnpF+EaQ72u8XhTc
20+
fTAb3XXNKB3O0DhRxN1vxVYKavZV71jTF7vKq08TVf52peFQ9j+r6IiSfL8bMIy5
21+
E1KN5DxvdHXUlJ3bBoN9KVECgYADGCBo2ASWGSocAHxQlwu39QQhdIhy+N483mhF
22+
4uEQn0a90Y3fXfge7vlhxahh9MxcNGDYqlwSq3frUzcwcnmndMBhYVBbIGQXVvy8
23+
rZHja8sk8Z7M8LPnXC/PQOF8QY1ZmTqyldiVB8K0SDGo/U3JW6kip2kKYsFhoGYx
24+
BHOg9QKBgAgClFnzxWoCA6EzwJM/zi5ulHqbkA5quPTfoFlsat30oCq/zCZFGBCt
25+
exYQ5nA1qO6FxHsWEnGcZySmpoHI8E+s/nFLGtb4M4KE+Kc96pWPxKyqGDepvyWK
26+
GNegmvzaTa5JqA2zmDQw0j4xhdgdhjdvyBt6tEVdWZKjB13Cax5B
27+
-----END RSA PRIVATE KEY-----
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# https://www.websequencediagrams.com/
2+
3+
RemoteControl->Controller: establish TCP connection
4+
Controller->Controller: generate new token
5+
Controller->RemoteControl: ControllerReply ( token )
6+
RemoteControl->Controller: RemoteControlRegistration ( team, [ token, signature ] )
7+
Controller-->Controller: verify token
8+
Controller-->Controller: verify signature
9+
Controller->RemoteControl: ControllerReply ( ok | reject )
10+
11+
loop
12+
RemoteControl->Controller: RemoteControlToController
13+
Controller->RemoteControl: ControllerReply ( ok | reject )
14+
end

cmd/ssl-remote-control-client/main.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"crypto/rsa"
6+
"flag"
7+
"fmt"
8+
"github.com/RoboCup-SSL/ssl-game-controller/internal/app/rcon"
9+
"github.com/RoboCup-SSL/ssl-game-controller/internal/app/state"
10+
"github.com/RoboCup-SSL/ssl-game-controller/pkg/client"
11+
"github.com/RoboCup-SSL/ssl-go-tools/pkg/sslconn"
12+
"github.com/golang/protobuf/proto"
13+
"log"
14+
"net"
15+
"os"
16+
"strconv"
17+
"strings"
18+
"time"
19+
)
20+
21+
var udpAddress = flag.String("udpAddress", "224.5.23.1:10003", "The multicast address of ssl-game-controller")
22+
var autoDetectAddress = flag.Bool("autoDetectHost", true, "Automatically detect the game-controller host and replace it with the host given in address")
23+
var refBoxAddr = flag.String("address", "localhost:10011", "Address to connect to")
24+
var privateKeyLocation = flag.String("privateKey", "", "A private key to be used to sign messages")
25+
var team = flag.String("team", "YELLOW", "The team to control, either YELLOW or BLUE")
26+
27+
var privateKey *rsa.PrivateKey
28+
29+
type Client struct {
30+
conn net.Conn
31+
token string
32+
}
33+
34+
func main() {
35+
flag.Parse()
36+
37+
privateKey = client.LoadPrivateKey(*privateKeyLocation)
38+
39+
if *autoDetectAddress {
40+
log.Print("Trying to detect host based on incoming referee messages...")
41+
host := client.DetectHost(*udpAddress)
42+
if host != "" {
43+
log.Print("Detected game-controller host: ", host)
44+
*refBoxAddr = client.GetConnectionString(*refBoxAddr, host)
45+
}
46+
}
47+
48+
conn, err := net.Dial("tcp", *refBoxAddr)
49+
if err != nil {
50+
log.Fatal("could not connect to game-controller at ", *refBoxAddr)
51+
}
52+
defer func() {
53+
if err := conn.Close(); err != nil {
54+
log.Printf("Could not close connection: %v", err)
55+
}
56+
}()
57+
log.Printf("Connected to game-controller at %v", *refBoxAddr)
58+
c := Client{}
59+
c.conn = conn
60+
61+
c.register()
62+
63+
commands := map[string]func([]string){}
64+
commands["ping"] = func(args []string) {
65+
c.sendRequest(&rcon.RemoteControlToController{
66+
Msg: &rcon.RemoteControlToController_Request_{Request: rcon.RemoteControlToController_PING},
67+
})
68+
}
69+
commands["keeper"] = func(args []string) {
70+
if len(args) != 1 {
71+
log.Printf("Missing keeper id")
72+
} else if id, err := strconv.Atoi(args[0]); err != nil {
73+
log.Printf("Could not parse keeper id '%v'", args[0])
74+
} else {
75+
c.sendDesiredKeeper(int32(id))
76+
}
77+
}
78+
commands["substitution"] = func(args []string) {
79+
c.sendRequest(&rcon.RemoteControlToController{
80+
Msg: &rcon.RemoteControlToController_SubstituteBot{SubstituteBot: true},
81+
})
82+
}
83+
commands["no_substitution"] = func(args []string) {
84+
c.sendRequest(&rcon.RemoteControlToController{
85+
Msg: &rcon.RemoteControlToController_SubstituteBot{SubstituteBot: false},
86+
})
87+
}
88+
commands["timeout"] = func(args []string) {
89+
c.sendRequest(&rcon.RemoteControlToController{
90+
Msg: &rcon.RemoteControlToController_Timeout{Timeout: true},
91+
})
92+
}
93+
commands["no_timeout"] = func(args []string) {
94+
c.sendRequest(&rcon.RemoteControlToController{
95+
Msg: &rcon.RemoteControlToController_Timeout{Timeout: false},
96+
})
97+
}
98+
commands["challenge"] = func(args []string) {
99+
c.sendRequest(&rcon.RemoteControlToController{
100+
Msg: &rcon.RemoteControlToController_Request_{Request: rcon.RemoteControlToController_CHALLENGE_FLAG},
101+
})
102+
}
103+
commands["emergency"] = func(args []string) {
104+
c.sendRequest(&rcon.RemoteControlToController{
105+
Msg: &rcon.RemoteControlToController_EmergencyStop{EmergencyStop: true},
106+
})
107+
}
108+
commands["no_emergency"] = func(args []string) {
109+
c.sendRequest(&rcon.RemoteControlToController{
110+
Msg: &rcon.RemoteControlToController_EmergencyStop{EmergencyStop: false},
111+
})
112+
}
113+
commands["state"] = func(args []string) {
114+
c.sendRequest(&rcon.RemoteControlToController{
115+
Msg: &rcon.RemoteControlToController_Request_{Request: rcon.RemoteControlToController_GET_STATE},
116+
})
117+
}
118+
119+
reader := bufio.NewReader(os.Stdin)
120+
for {
121+
fmt.Print("-> ")
122+
text, err := reader.ReadString('\n')
123+
if err != nil {
124+
log.Print("Can not read from stdin: ", err)
125+
for {
126+
time.Sleep(1 * time.Second)
127+
}
128+
}
129+
// convert CRLF to LF
130+
text = strings.Replace(text, "\n", "", -1)
131+
cmd := strings.Split(text, " ")
132+
if fn, ok := commands[cmd[0]]; ok {
133+
fn(cmd[1:])
134+
} else {
135+
fmt.Println("Available commands:")
136+
for cmd := range commands {
137+
fmt.Printf(" %-20s\n", cmd)
138+
}
139+
}
140+
}
141+
}
142+
143+
func (c *Client) register() {
144+
reply := rcon.ControllerToRemoteControl{}
145+
if err := sslconn.ReceiveMessage(c.conn, &reply); err != nil {
146+
log.Fatal("Failed receiving controller reply: ", err)
147+
}
148+
if reply.GetControllerReply().NextToken == nil {
149+
log.Fatal("Missing next token")
150+
}
151+
152+
registration := rcon.RemoteControlRegistration{}
153+
registration.Team = new(state.Team)
154+
*registration.Team = state.Team(state.Team_value[*team])
155+
if privateKey != nil {
156+
registration.Signature = &rcon.Signature{Token: reply.GetControllerReply().NextToken, Pkcs1V15: []byte{}}
157+
registration.Signature.Pkcs1V15 = client.Sign(privateKey, &registration)
158+
}
159+
log.Print("Sending registration")
160+
if err := sslconn.SendMessage(c.conn, &registration); err != nil {
161+
log.Fatal("Failed sending registration: ", err)
162+
}
163+
log.Print("Sent registration, waiting for reply")
164+
reply = rcon.ControllerToRemoteControl{}
165+
if err := sslconn.ReceiveMessage(c.conn, &reply); err != nil {
166+
log.Fatal("Failed receiving controller reply: ", err)
167+
}
168+
if reply.GetControllerReply().StatusCode == nil || *reply.GetControllerReply().StatusCode != rcon.ControllerReply_OK {
169+
reason := ""
170+
if reply.GetControllerReply().Reason != nil {
171+
reason = *reply.GetControllerReply().Reason
172+
}
173+
log.Fatal("Registration rejected: ", reason)
174+
}
175+
log.Printf("Successfully registered as %v", *team)
176+
if reply.GetControllerReply().NextToken != nil {
177+
c.token = *reply.GetControllerReply().NextToken
178+
} else {
179+
c.token = ""
180+
}
181+
}
182+
183+
func (c *Client) sendDesiredKeeper(id int32) (accepted bool) {
184+
message := rcon.RemoteControlToController_DesiredKeeper{DesiredKeeper: id}
185+
request := rcon.RemoteControlToController{Msg: &message}
186+
return c.sendRequest(&request)
187+
}
188+
189+
func (c *Client) sendRequest(request *rcon.RemoteControlToController) (accepted bool) {
190+
if privateKey != nil {
191+
request.Signature = &rcon.Signature{Token: &c.token, Pkcs1V15: []byte{}}
192+
request.Signature.Pkcs1V15 = client.Sign(privateKey, request)
193+
}
194+
195+
log.Print("Sending ", proto.MarshalTextString(request))
196+
197+
if err := sslconn.SendMessage(c.conn, request); err != nil {
198+
log.Fatalf("Failed sending request: %v (%v)", request, err)
199+
}
200+
201+
log.Print("Waiting for reply...")
202+
reply := rcon.ControllerToRemoteControl{}
203+
if err := sslconn.ReceiveMessage(c.conn, &reply); err != nil {
204+
log.Fatal("Failed receiving controller reply: ", err)
205+
}
206+
log.Print("Received reply: ", proto.MarshalTextString(&reply))
207+
if reply.GetControllerReply().StatusCode == nil || *reply.GetControllerReply().StatusCode != rcon.ControllerReply_OK {
208+
log.Print("Message rejected: ", *reply.GetControllerReply().Reason)
209+
accepted = false
210+
} else {
211+
accepted = true
212+
}
213+
214+
if reply.GetControllerReply().NextToken != nil {
215+
c.token = *reply.GetControllerReply().NextToken
216+
} else {
217+
c.token = ""
218+
}
219+
220+
return
221+
}

0 commit comments

Comments
 (0)