Project update - Battleship - P2P and web-browser support
In my previous post, I introduced my hobby project, a two-player battleship game, playable through TUIs, where one terminal hosts the game, and another terminal joins it. I did want to take it further than just a simple client-server application driven through a TUI. So, here I mention some progress, along with learnings. The updated codebase is v0.0.2.
Connecting the apps over Internet using P2P
For the game to work right now, the joining-client needs to be able to connect to the hosting-server. Therefore, the server has to be reachable from the client, in terms of IP-address and port. It is a WebSocket over TCP/IP connection. There might be some firewall configuration required on the server machine to allow connections to the incoming port. This is a relatively simple setup, and works nicely for most LANs.
One thing I'd like to support is not just this sort of connectivity, but also making it possible to connect over the Internet from any client to any server. I want to take this opportunity to learn about P2P networks.
But, there are some challenges along the way.
NATs
Most domestic users' networks are behind routers doing NAT. The user's machine IP-address (usually private, allocated internally) is different than their public IP-address (assigned by the ISP). For outgoing connections, ports are usually remapped as well.
Applications can use STUN servers to try to somewhat discover these public endpoints and NAT types, followed by UDP hole-punching, for example, to establish direct peer connections.
This is not always possible. Modern or commercial grade routers can also block incoming packets, unless it's a reply to a connection initiated by it. Symmetric NATs can always be problematic, as in my case. That's where, TURN relay servers come in. We have to use them as a fallback.
And then we have ICE, which is a framework meant to help figure out the best approach (direct or STUN hole punching or TURN relay) by testing every path quickly and picking the fastest route.
So, I elected to use coturn, which looks like a nice way to help this. I'll need to host this publicly.
To clarify, I could just build a bespoke dedicated centralised game-server, that all players (hosters and joiners) connect to as clients and communicate through. But, this can be hard to run/manage and get right in the first place. It'll be is very specific to my game. And over here I want to support P2P networking too.
Signalling server
We also need some sort of signalling server. This is a small server that acts like a matchmaker, allowing joining-clients to find hosting-servers. In the case of battleship, a UUID is used for matching. I've written this in Rust.
Web-browser mode
Another thing I'd like to support is to be able to play the game in the web-browser. I'd like this to be similar to the TUI setup, where one player hosts a game and another player joins it. But what's more, I'd like to support a way for both terminals and web-browsers to play together.
For the battleship GUI, I went with Leptos in client-side rendering mode, this allows me to build a WebAssembly based client in Rust. And it means I get to learn a popular Rust GUI framework. I also went with the same terminal look in the browser, which I thought was nice.
For the P2P networking connectivity mentioned in the previous section, we have to use WebRTC, as that's the technology available in web-browsers for this sort of networking.
Screenshots/Traces
I haven't hosted anything publicly, yet. But, here are some screenshots that allow me to play locally.
- I have configured coturn and restarted the service.
The /etc/turnserver.conf has this:
listening-port=3478
realm=battleship.local
use-auth-secret
static-auth-secret=nolan
allow-loopback-peers
cli-password=nolan
Status after restart shows a sensible output:
nolan@nolan-veed:~/projects/battleship$ sudo systemctl restart coturn
nolan@nolan-veed:~/projects/battleship$ sudo systemctl status coturn
● coturn.service - coTURN STUN/TURN Server
Loaded: loaded (/usr/lib/systemd/system/coturn.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-11-03 08:02:38 GMT; 2s ago
Invocation: 85821083156e4d66997063ef18b26a94
Docs: man:coturn(1)
man:turnadmin(1)
man:turnserver(1)
Main PID: 897772 (turnserver)
Tasks: 27 (limit: 37498)
Memory: 12.6M (peak: 13.3M)
CPU: 38ms
CGroup: /system.slice/coturn.service
└─897772 /usr/bin/turnserver -c /etc/turnserver.conf --pidfile=
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (admin thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : SQLite DB connection success: /var/lib/turn/turndb
Nov 03 08:02:38 nolan-veed systemd[1]: Started coturn.service - coTURN STUN/TURN Server.
- I then launch my signalling server - I've just configured the simple TURN secret for now.
nolan@nolan-veed:~/projects/battleship$ cargo run --release -p battleship-signal -- --turn-secret nolan
Finished `release` profile [optimized] target(s) in 0.17s
Running `target/release/battleship-signal --turn-secret nolan`
2026-11-03T08:04:57.822311Z INFO battleship_signal: signaling server listening on 0.0.0.0:3536
- I then launch my web-browser app for the hosting player.
nolan@nolan-veed:~/projects/battleship/crates/web$ ~/.cargo/bin/trunk serve
2026-11-03T08:06:47.242180Z INFO 🚀 Starting trunk 0.22.1
2026-11-03T08:06:47.242968Z INFO 📦 starting build
2026-11-03T08:06:47.243056Z INFO node packages []
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
2026-11-03T08:06:48.667422Z INFO applying new distribution
2026-11-03T08:06:48.668784Z INFO ✅ success
2026-11-03T08:06:48.668819Z INFO 📡 serving static assets at -> /
2026-11-03T08:06:48.669063Z INFO 📡 server listening at:
2026-11-03T08:06:48.669069Z INFO 🏠 http://127.0.0.1:8080/
2026-11-03T08:06:48.669073Z INFO 🏠 http://[::1]:8080/
2026-11-03T08:06:48.669124Z INFO 🏠 http://localhost.:8080/
2026-11-03T08:06:48.669135Z INFO 🏠 http://ip6-loopback.:8080/
2026-11-03T08:06:48.669138Z INFO 🏠 http://ip6-localhost.:8080/

- I can then join by TUI, you can see I have to specify the signalling and TURN server.
nolan@nolan-veed:~/projects/battleship$ cargo run --release -p battleship-tui -- join 8ffe82e8-46a8-4fa1-bcd5-74d300876232 --signal ws://localhost:3536 --turn turn:localhost:3478
Signalling server says:
2026-11-03T08:08:03.872762Z INFO battleship_signal::rooms: host registered room=8ffe82e8
2026-11-03T08:11:14.393155Z INFO battleship_signal::rooms: joiner arrived room=8ffe82e8
- Both players can place ships.


- Both players can play.


- I will quit the joining player, and rejoin in the browser.
Signalling server says:
2026-11-03T08:19:55.404952Z INFO battleship_signal::rooms: joiner left room=8ffe82e8
2026-11-03T08:20:42.741570Z INFO battleship_signal::rooms: joiner arrived room=8ffe82e8

Looking ahead
This has been good for learning. The claude has made it easy to figure out the moving pieces. I've made sure the code structure is good, and the code is not too messy.
I need to figure out a couple of things though. I need a better way to host these coturn and signaling servers. I have found some cloud-based providers for TURN servers and for signaling. So, watch this space for updates there. It'll make it easier for me to host battleship on this website.
- ← Previous
Hobby project - Battleship