diff --git a/src/api/client/voip.rs b/src/api/client/voip.rs
index c5ea96f9be30b81871163eab47b20627f2beefdd..9608dc88fe30782a8921f1a05ee00d8c96b30188 100644
--- a/src/api/client/voip.rs
+++ b/src/api/client/voip.rs
@@ -1,12 +1,15 @@
 use std::time::{Duration, SystemTime};
 
 use base64::{engine::general_purpose, Engine as _};
+use conduit::utils;
 use hmac::{Hmac, Mac};
-use ruma::{api::client::voip::get_turn_server_info, SecondsSinceUnixEpoch};
+use ruma::{api::client::voip::get_turn_server_info, SecondsSinceUnixEpoch, UserId};
 use sha1::Sha1;
 
 use crate::{services, Result, Ruma};
 
+const RANDOM_USER_ID_LENGTH: usize = 10;
+
 type HmacSha1 = Hmac<Sha1>;
 
 /// # `GET /_matrix/client/r0/voip/turnServer`
@@ -15,8 +18,6 @@
 pub(crate) async fn turn_server_route(
 	body: Ruma<get_turn_server_info::v3::Request>,
 ) -> Result<get_turn_server_info::v3::Response> {
-	let sender_user = body.sender_user.as_ref().expect("user is authenticated");
-
 	let turn_secret = services().globals.turn_secret().clone();
 
 	let (username, password) = if !turn_secret.is_empty() {
@@ -27,7 +28,15 @@ pub(crate) async fn turn_server_route(
 		)
 		.expect("time is valid");
 
-		let username: String = format!("{}:{}", expiry.get(), sender_user);
+		let user = body.sender_user.unwrap_or_else(|| {
+			UserId::parse_with_server_name(
+				utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
+				&services().globals.config.server_name,
+			)
+			.unwrap()
+		});
+
+		let username: String = format!("{}:{}", expiry.get(), user);
 
 		let mut mac = HmacSha1::new_from_slice(turn_secret.as_bytes()).expect("HMAC can take key of any size");
 		mac.update(username.as_bytes());
diff --git a/src/api/ruma_wrapper/auth.rs b/src/api/ruma_wrapper/auth.rs
index 35fd443de5274aa6f1e373be18383856bfd4d605..f6dcc01f50b3e81ab1d90e5686d4b29241840d4e 100644
--- a/src/api/ruma_wrapper/auth.rs
+++ b/src/api/ruma_wrapper/auth.rs
@@ -91,8 +91,21 @@ pub(super) async fn auth(
 				appservice_info: Some(*info),
 			})
 		},
-		(AuthScheme::AccessToken, Token::None) => {
-			Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))
+		(AuthScheme::AccessToken, Token::None) => match request.parts.uri.path() {
+			// TODO: can we check this better?
+			"/_matrix/client/v3/voip/turnServer" | "/_matrix/client/r0/voip/turnServer" => {
+				if services().globals.config.turn_allow_guests {
+					Ok(Auth {
+						origin: None,
+						sender_user: None,
+						sender_device: None,
+						appservice_info: None,
+					})
+				} else {
+					Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))
+				}
+			},
+			_ => Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token.")),
 		},
 		(
 			AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs
index 6c2c965575f9765fd843f2be6f670091319fa73a..ae9902e1165c17e9873c9cdb997340f0cceb5844 100644
--- a/src/core/config/mod.rs
+++ b/src/core/config/mod.rs
@@ -165,6 +165,8 @@ pub struct Config {
 	#[serde(default)]
 	pub allow_public_room_directory_without_auth: bool,
 	#[serde(default)]
+	pub turn_allow_guests: bool,
+	#[serde(default)]
 	pub lockdown_public_room_directory: bool,
 	#[serde(default)]
 	pub allow_device_name_federation: bool,