feat(network): add selectable privacy-preserving discovery
This commit is contained in:
parent
9cf0c8fc9c
commit
4d31fd1517
26 changed files with 959 additions and 153 deletions
|
|
@ -6,7 +6,7 @@ signal browse_status_changed(message: String, is_error: bool)
|
|||
signal host_settings_changed(room_name: String, discoverable: bool)
|
||||
signal host_status_changed(message: String, is_error: bool)
|
||||
signal host_state_changed(state: int)
|
||||
signal public_join_prepared(endpoint: String)
|
||||
signal public_join_prepared(route: ConnectionRoute)
|
||||
signal public_join_status_changed(message: String, is_error: bool)
|
||||
signal public_join_state_changed(state: int)
|
||||
signal friend_presence_updated(friends: Array[Dictionary])
|
||||
|
|
@ -28,6 +28,9 @@ const LEGACY_DEFAULT_ROOM_NAME: String = "straywild Room"
|
|||
const LEGACY_DEDICATED_DEFAULT_ROOM_NAME: String = (
|
||||
"straywild Dedicated Server"
|
||||
)
|
||||
const HOST_CONNECTION_DIRECT: String = "direct"
|
||||
const HOST_CONNECTION_RELAY: String = "relay"
|
||||
const HOST_DISCOVERY_PRIVATE: String = "private"
|
||||
const ROOM_NAME_SUFFIX: String = "'s Server"
|
||||
const MAX_ROOM_NAME_LENGTH: int = 48
|
||||
const HEARTBEAT_INTERVAL_SECONDS: float = 15.0
|
||||
|
|
@ -35,6 +38,7 @@ const REQUEST_TIMEOUT_SECONDS: float = 8.0
|
|||
const TRAVERSAL_POLL_INTERVAL_SECONDS: float = 1.0
|
||||
const JOIN_PROBE_INTERVAL_SECONDS: float = 0.35
|
||||
const TRAVERSAL_PACKET_PREFIX: String = "straywild_TRAVERSAL_V1 "
|
||||
const RELAY_AUTH_PACKET_PREFIX: String = "straywild_RELAY_AUTH_V1 "
|
||||
const UPNP_MAPPING_DURATION_SECONDS: int = 3600
|
||||
const UPNP_RENEW_INTERVAL_SECONDS: float = 2700.0
|
||||
const UPNP_RETRY_INTERVAL_SECONDS: float = 300.0
|
||||
|
|
@ -90,6 +94,7 @@ var _join_request: HTTPRequest
|
|||
var _traversal_timer: Timer
|
||||
var _join_probe_timer: Timer
|
||||
var _host_verified: bool = false
|
||||
var _host_connection_preference: String = HOST_CONNECTION_DIRECT
|
||||
var _traversal_host: String = ""
|
||||
var _traversal_port: int = 0
|
||||
var _host_verification_token: String = ""
|
||||
|
|
@ -97,6 +102,8 @@ var _join_request_in_flight: bool = false
|
|||
var _pending_join_endpoint: String = ""
|
||||
var _pending_join_token: String = ""
|
||||
var _pending_join_room_id: String = ""
|
||||
var _pending_join_room_name: String = ""
|
||||
var _pending_join_transport: String = ""
|
||||
var _preserve_pending_join_on_inactive: bool = false
|
||||
var _upnp_thread: Thread
|
||||
var _upnp_mapping_in_progress: bool = false
|
||||
|
|
@ -381,12 +388,25 @@ func get_host_state() -> HostState:
|
|||
return _host_state
|
||||
|
||||
|
||||
func get_host_connection_mode() -> String:
|
||||
return _host_connection_preference
|
||||
|
||||
|
||||
func get_host_discovery_mode() -> String:
|
||||
return (
|
||||
_host_connection_preference
|
||||
if _discoverable
|
||||
else HOST_DISCOVERY_PRIVATE
|
||||
)
|
||||
|
||||
|
||||
func get_public_join_state() -> PublicJoinState:
|
||||
return _public_join_state
|
||||
|
||||
|
||||
func configure_dedicated_runtime(room_name: String) -> bool:
|
||||
_host_settings_persistence_enabled = false
|
||||
_host_connection_preference = HOST_CONNECTION_DIRECT
|
||||
return set_room_name(room_name)
|
||||
|
||||
|
||||
|
|
@ -453,6 +473,49 @@ func set_discoverable(enabled: bool) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func set_host_connection_mode(connection_mode: String) -> bool:
|
||||
if connection_mode not in [
|
||||
HOST_CONNECTION_DIRECT,
|
||||
HOST_CONNECTION_RELAY,
|
||||
]:
|
||||
return false
|
||||
if (
|
||||
connection_mode == HOST_CONNECTION_RELAY
|
||||
and _session != null
|
||||
and _session.is_dedicated_host()
|
||||
):
|
||||
_set_host_state(HostState.ERROR)
|
||||
_set_host_status(
|
||||
"Dedicated servers must use direct discovery hosting.", true
|
||||
)
|
||||
return false
|
||||
if _host_connection_preference == connection_mode:
|
||||
return true
|
||||
_host_connection_preference = connection_mode
|
||||
_save_settings()
|
||||
host_settings_changed.emit(_room_name, _discoverable)
|
||||
if _discoverable:
|
||||
_set_host_state(HostState.REGISTERING)
|
||||
_set_host_status("Updating public connection mode…", false)
|
||||
_synchronize_host_lease()
|
||||
return true
|
||||
|
||||
|
||||
func cycle_host_discovery_mode() -> bool:
|
||||
match get_host_discovery_mode():
|
||||
HOST_DISCOVERY_PRIVATE:
|
||||
if not set_host_connection_mode(HOST_CONNECTION_DIRECT):
|
||||
return false
|
||||
return set_discoverable(true)
|
||||
HOST_CONNECTION_DIRECT:
|
||||
if _session != null and _session.is_dedicated_host():
|
||||
return set_discoverable(false)
|
||||
return set_host_connection_mode(HOST_CONNECTION_RELAY)
|
||||
HOST_CONNECTION_RELAY:
|
||||
return set_discoverable(false)
|
||||
return false
|
||||
|
||||
|
||||
func is_public_join_preparing() -> bool:
|
||||
return _join_request_in_flight
|
||||
|
||||
|
|
@ -480,13 +543,16 @@ func prepare_public_join(room: Dictionary) -> bool:
|
|||
"You are already hosting this room.", true
|
||||
)
|
||||
return false
|
||||
var endpoint: String = room_endpoint(room)
|
||||
var room_id: String = str(room.get("room_id", "")).strip_edges()
|
||||
if endpoint.is_empty() or room_id.is_empty():
|
||||
if room_id.is_empty():
|
||||
_set_public_join_state(PublicJoinState.ERROR)
|
||||
return false
|
||||
_pending_join_endpoint = endpoint
|
||||
_pending_join_endpoint = ""
|
||||
_pending_join_room_id = room_id
|
||||
_pending_join_room_name = str(
|
||||
room.get("room_name", "public room")
|
||||
).strip_edges()
|
||||
_pending_join_transport = str(room.get("connection_mode", ""))
|
||||
_pending_join_token = ""
|
||||
var url: String = "%s/v1/rooms/%s/join-attempts" % [
|
||||
_base_url,
|
||||
|
|
@ -535,16 +601,6 @@ func request_rooms() -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func room_endpoint(room: Dictionary) -> String:
|
||||
var address: String = str(room.get("address", "")).strip_edges()
|
||||
var port: int = int(room.get("port", 0))
|
||||
if address.is_empty() or port < 1 or port > 65535:
|
||||
return ""
|
||||
if ":" in address and not address.begins_with("["):
|
||||
address = "[%s]" % address
|
||||
return "%s:%d" % [address, port]
|
||||
|
||||
|
||||
func is_own_room(room: Dictionary) -> bool:
|
||||
return (
|
||||
not _lease_room_id.is_empty()
|
||||
|
|
@ -640,6 +696,14 @@ func _host_payload() -> Dictionary:
|
|||
ProjectSettings.get_setting("application/config/version", "unknown")
|
||||
),
|
||||
"protocol_version": NetworkProtocol.PROTOCOL_VERSION,
|
||||
"host_kind": (
|
||||
"dedicated" if _session.is_dedicated_host() else "player"
|
||||
),
|
||||
"connection_mode": (
|
||||
HOST_CONNECTION_DIRECT
|
||||
if _session.is_dedicated_host()
|
||||
else _host_connection_preference
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -688,7 +752,7 @@ func _on_host_request_completed(
|
|||
else HostState.VERIFYING
|
||||
)
|
||||
_set_host_status(
|
||||
"Room is listed publicly."
|
||||
_host_listing_status(room)
|
||||
if _host_verified
|
||||
else "Checking the public route…",
|
||||
false,
|
||||
|
|
@ -706,7 +770,7 @@ func _on_host_request_completed(
|
|||
else HostState.VERIFYING
|
||||
)
|
||||
_set_host_status(
|
||||
"Room is listed publicly."
|
||||
_host_listing_status(room)
|
||||
if _host_verified
|
||||
else "Checking the public route…",
|
||||
false,
|
||||
|
|
@ -755,10 +819,33 @@ func _on_join_request_completed(
|
|||
return
|
||||
_pending_join_token = str(response.get("join_token", ""))
|
||||
_apply_traversal_response(response)
|
||||
var route_transport := ""
|
||||
var route_privacy := ""
|
||||
var route_details: Variant = response.get("route", {})
|
||||
if typeof(route_details) == TYPE_DICTIONARY:
|
||||
var direct_route: Dictionary = route_details
|
||||
route_transport = str(direct_route.get("transport", ""))
|
||||
route_privacy = str(direct_route.get("ip_privacy", ""))
|
||||
if route_transport in ["direct", "relay"]:
|
||||
var address := str(direct_route.get("address", "")).strip_edges()
|
||||
var port := int(direct_route.get("port", 0))
|
||||
if not address.is_empty() and port >= 1 and port <= 65535:
|
||||
if ":" in address and not address.begins_with("["):
|
||||
address = "[%s]" % address
|
||||
_pending_join_endpoint = "%s:%d" % [address, port]
|
||||
if (
|
||||
_pending_join_token.is_empty()
|
||||
or _traversal_host.is_empty()
|
||||
or _traversal_port < 1
|
||||
or _pending_join_endpoint.is_empty()
|
||||
or route_transport != _pending_join_transport
|
||||
or (
|
||||
route_transport == "direct"
|
||||
and route_privacy != "peer_visible"
|
||||
)
|
||||
or (route_transport == "relay" and route_privacy != "relayed")
|
||||
or (
|
||||
route_transport == "direct"
|
||||
and (_traversal_host.is_empty() or _traversal_port < 1)
|
||||
)
|
||||
):
|
||||
_set_public_join_state(PublicJoinState.ERROR)
|
||||
public_join_status_changed.emit(
|
||||
|
|
@ -768,7 +855,25 @@ func _on_join_request_completed(
|
|||
return
|
||||
_set_public_join_state(PublicJoinState.CONNECTING)
|
||||
public_join_status_changed.emit("Connecting…", false)
|
||||
public_join_prepared.emit(_pending_join_endpoint)
|
||||
var endpoint := EndpointParser.parse(_pending_join_endpoint)
|
||||
if not endpoint.is_valid():
|
||||
_set_public_join_state(PublicJoinState.ERROR)
|
||||
public_join_status_changed.emit("Could not reach this room.", true)
|
||||
_clear_pending_join()
|
||||
return
|
||||
public_join_prepared.emit(
|
||||
ConnectionRoute.discovery_relay(
|
||||
endpoint,
|
||||
_pending_join_room_id,
|
||||
_pending_join_room_name,
|
||||
)
|
||||
if route_transport == "relay"
|
||||
else ConnectionRoute.discovery_direct(
|
||||
endpoint,
|
||||
_pending_join_room_id,
|
||||
_pending_join_room_name,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _on_traversal_timer_timeout() -> void:
|
||||
|
|
@ -856,6 +961,15 @@ func _send_pending_join_probe() -> void:
|
|||
):
|
||||
_join_probe_timer.stop()
|
||||
return
|
||||
if _pending_join_transport == "relay":
|
||||
var relay_endpoint := EndpointParser.parse(_pending_join_endpoint)
|
||||
if relay_endpoint.is_valid():
|
||||
_session.send_traversal_packet(
|
||||
relay_endpoint.host,
|
||||
relay_endpoint.port,
|
||||
(RELAY_AUTH_PACKET_PREFIX + _pending_join_token).to_utf8_buffer(),
|
||||
)
|
||||
return
|
||||
_send_traversal_probe({
|
||||
"kind": "join",
|
||||
"room_id": _pending_join_room_id,
|
||||
|
|
@ -1013,10 +1127,24 @@ func _valid_public_room(room: Dictionary) -> bool:
|
|||
return (
|
||||
typeof(room.get("room_id")) == TYPE_STRING
|
||||
and typeof(room.get("room_name")) == TYPE_STRING
|
||||
and typeof(room.get("address")) == TYPE_STRING
|
||||
and typeof(room.get("game_version")) == TYPE_STRING
|
||||
and str(room.get("game_version")) == expected_version
|
||||
and _valid_json_integer(room.get("port"), 1, 65535)
|
||||
and str(room.get("connection_mode", "")) in ["direct", "relay"]
|
||||
and (
|
||||
(
|
||||
str(room.get("connection_mode", "")) == "direct"
|
||||
and str(room.get("ip_privacy", "")) == "peer_visible"
|
||||
)
|
||||
or (
|
||||
str(room.get("connection_mode", "")) == "relay"
|
||||
and str(room.get("ip_privacy", "")) == "relayed"
|
||||
)
|
||||
)
|
||||
and str(room.get("host_kind", "")) in ["player", "dedicated"]
|
||||
and not (
|
||||
str(room.get("host_kind", "")) == "dedicated"
|
||||
and str(room.get("connection_mode", "")) == "relay"
|
||||
)
|
||||
and _valid_json_integer(room.get("current_players"), 0, 128)
|
||||
and _valid_json_integer(room.get("max_players"), 1, 128)
|
||||
and int(room["current_players"]) <= int(room["max_players"])
|
||||
|
|
@ -1370,6 +1498,18 @@ func _request_failure(response: Dictionary) -> String:
|
|||
return "Room discovery is temporarily unavailable."
|
||||
|
||||
|
||||
func _host_listing_status(room: Dictionary) -> String:
|
||||
if str(room.get("connection_mode", "")) == "relay":
|
||||
return (
|
||||
"Room is listed through the privacy relay. Players see a relay "
|
||||
+ "address; the relay service still handles network addresses."
|
||||
)
|
||||
return (
|
||||
"Room is listed. Its address is hidden while browsing, but direct "
|
||||
+ "players can see your public IP after joining."
|
||||
)
|
||||
|
||||
|
||||
func _discovery_version_mismatch_message(required_version: String) -> String:
|
||||
var local_version: String = NetworkProtocol.game_version()
|
||||
var rejection: NetworkProtocol.RejectionCode = (
|
||||
|
|
@ -1493,6 +1633,8 @@ func _clear_pending_join() -> void:
|
|||
_pending_join_endpoint = ""
|
||||
_pending_join_token = ""
|
||||
_pending_join_room_id = ""
|
||||
_pending_join_room_name = ""
|
||||
_pending_join_transport = ""
|
||||
_join_probe_timer.stop()
|
||||
|
||||
|
||||
|
|
@ -1571,6 +1713,14 @@ func _load_settings() -> void:
|
|||
_presence_sharing = bool(config.get_value(
|
||||
"social", "share_presence", false
|
||||
))
|
||||
var saved_connection_mode := str(config.get_value(
|
||||
"host", "connection_mode", HOST_CONNECTION_DIRECT
|
||||
))
|
||||
if saved_connection_mode in [
|
||||
HOST_CONNECTION_DIRECT,
|
||||
HOST_CONNECTION_RELAY,
|
||||
]:
|
||||
_host_connection_preference = saved_connection_mode
|
||||
|
||||
|
||||
func _save_settings() -> void:
|
||||
|
|
@ -1581,6 +1731,9 @@ func _save_settings() -> void:
|
|||
config.set_value(
|
||||
"host", "room_name_uses_default", _room_name_uses_default
|
||||
)
|
||||
config.set_value(
|
||||
"host", "connection_mode", _host_connection_preference
|
||||
)
|
||||
config.set_value("social", "share_presence", _presence_sharing)
|
||||
var error: Error = config.save(SETTINGS_PATH)
|
||||
if error != OK:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue