diff --git a/ts3-client/core/src/main/java/com/ts3client/config/ClientVersion.java b/ts3-client/core/src/main/java/com/ts3client/config/ClientVersion.java
new file mode 100644
index 0000000..f8428b3
--- /dev/null
+++ b/ts3-client/core/src/main/java/com/ts3client/config/ClientVersion.java
@@ -0,0 +1,88 @@
+package com.ts3client.config;
+
+import java.util.List;
+
+/**
+ * The version, platform and signature a client reports to the server in
+ * {@code clientinit}. Other clients see the version and platform in the info
+ * panel ("3.6.2 on Windows").
+ *
+ *
The signature is TeamSpeak's, over the version and platform strings
+ * together, so the three only work as a set. Servers answer "client is modified"
+ * for a version they do not accept — an invented one, or one older than their
+ * configured minimum — which is why {@link #PRESETS} holds genuine triples taken
+ * from released clients, newest first per platform.
+ *
+ * @param label human-readable name, e.g. "3.6.2 on Windows"
+ * @param version {@code client_version}, e.g. "3.6.2 [Build: 1695203293]"
+ * @param platform {@code client_platform}, e.g. "Windows"
+ * @param sign {@code client_version_sign}, base64
+ */
+public record ClientVersion(String label, String version, String platform, String sign) {
+
+ /** Platform names the official clients report. */
+ public static final List PLATFORMS =
+ List.of("Windows", "Linux", "OS X", "Android", "iOS");
+
+ /**
+ * Signed triples from released TeamSpeak clients. "3.?.?" is the version the
+ * SDK-based clients report; it is signed for every platform and is what this
+ * client uses by default.
+ */
+ public static final List PRESETS = List.of(
+ new ClientVersion("3.?.? on Windows", "3.?.? [Build: 5680278000]", "Windows",
+ "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="),
+ new ClientVersion("3.6.3 on Windows", "3.6.3 [Build: 1701166057]", "Windows",
+ "ML3yE6pV0QGyhc8o7k1bgqDSj+5gZIQFMjBNhWJ6L0FtJwniBq8l1wivx3DzgVVCvyRbzfg8MsdV/rjl5sHNCw=="),
+ new ClientVersion("3.6.2 on Windows", "3.6.2 [Build: 1695203293]", "Windows",
+ "4BdaZpdgUSMCuIs8qcloJPNxNlJ4o7QKnxMCRO60mSOTtJZyKjOrGLAmeAEtLIJjcjmdSpycMbQOIV92K2vXAw=="),
+ new ClientVersion("3.6.1 on Windows", "3.6.1 [Build: 1690872913]", "Windows",
+ "+YmPaYUBL4F9+TgxyFhlT8qIi1Ym+So4NNMcuohla/u4kfJCGabZ1vDF4cIOAghIqWfO+bQ0OkORZrCBdpi2Cw=="),
+ new ClientVersion("3.6.0 on Windows", "3.6.0 [Build: 1686749764]", "Windows",
+ "gfzG6CfwNYeqrjT68s7ahPd24A/XS0P8LLAB7hrE54MnrlO12RiflD2w69VZ7EYlqnd9GAiVzdGJHCmCFWtlAQ=="),
+
+ new ClientVersion("3.?.? on Linux", "3.?.? [Build: 5680278000]", "Linux",
+ "Hjd+N58Gv3ENhoKmGYy2bNRBsNNgm5kpiaQWxOj5HN2DXttG6REjymSwJtpJ8muC2gSwRuZi0R+8Laan5ts5CQ=="),
+ new ClientVersion("3.6.2 on Linux", "3.6.2 [Build: 1695203293]", "Linux",
+ "p4iF1jZ3ZOz9MEkKSZ2bvnFtm9WmUcQy9mAP//erFE4PF1sB6K1CSANrr+3X4B0aZR0u+K2pjnv8kiKsWKQCBQ=="),
+ new ClientVersion("3.6.1 on Linux", "3.6.1 [Build: 1690193193]", "Linux",
+ "UAcE5h5FgPcdChWTtiO816HX8f2qdoDcDEw6crbJ96Yo7rp1Bt+qx/rkKK9bzlVWMhOd4fI7pr++1XaPkEBnAQ=="),
+ new ClientVersion("3.6.0 on Linux", "3.6.0 [Build: 1686749764]", "Linux",
+ "TccRBHwn5LSA0lfpa2B42QmfCOiG+MqqyhlOqy5RcuRQNUGPD0e47lUz+Z/8+Gsdc1TPG4+vYlRl8+v2v+26Aw=="),
+ new ClientVersion("3.5.8 on Linux", "3.5.8 [Build: 1644595825]", "Linux",
+ "CK+cWJQESZgUdw1AFHINMbwiLjFGSiupour0Dfj/j1/xwx4L3nW3G+alYtiQw/wQwlWm873HS6PvXUBarz/tCw=="),
+
+ new ClientVersion("3.?.? on OS X", "3.?.? [Build: 5680278000]", "OS X",
+ "SttEnjoWE8jqIM6BOHSfiZP9DGjW0EP/ajU4bdKqgGMV4aYq/kzwVA9gxbmdIzV4lbaokvXBqrRjfBHrTVh8Cg=="),
+ new ClientVersion("3.6.2 on OS X", "3.6.2 [Build: 1695203293]", "OS X",
+ "SOCkLcsxINIGVc+3OQ5Vrmv2BVNNFA9LX5Jj9tqbIiYv2HU6Z5E8AL5+v5G2LDN0F+U55+uW43ezUoYAr1WuBA=="),
+ new ClientVersion("3.6.1 on OS X", "3.6.1 [Build: 1690193193]", "OS X",
+ "P+KOw42V8K0nN5FbGItCaJzrDuWcRgfE3MRzrtbYAhRMTXHzRxvwLkoLM7+vKTW135NtWZlKbZuDAvxgwhSLBg=="),
+ new ClientVersion("3.6.0 on OS X", "3.6.0 [Build: 1686749764]", "OS X",
+ "g8H7Op8eCrn0wHutuD/SlmUcZPmkSlFUBhvZpSgm9kuQl4dapzKJq2anI5jDwRs/ooBm/wrIlSiWI6HXuOPZDA=="),
+
+ new ClientVersion("3.?.? on Android", "3.?.? [Build: 5680278000]", "Android",
+ "AWb948BY32Z7bpIyoAlQguSmxOGcmjESPceQe1DpW5IZ4+AW1KfTk2VUIYNfUPsxReDJMCtlhVKslzhR2lf0AA=="),
+ new ClientVersion("3.8.7 on Android", "3.8.7 [Build: 1785186501]", "Android",
+ "mbt0WPhXJ+YKJUix5b6t+ZOHTCfvRqhhhrpcS5koM9CUL0BAFQZRG5Di5rJrqipZdk+gEUtpAX9I5nkuL7V9Dw=="),
+ new ClientVersion("3.8.6 on Android", "3.8.6 [Build: 1774892004]", "Android",
+ "Hh0oEZupsq8f0BuUHhMKdkXsMmgZZ03dJL8qAwnlH2JAHuefaAZFS/buiH2MLwgdRkZ81ffYk5YOTfc/bJczDw=="),
+
+ new ClientVersion("3.?.? on iOS", "3.?.? [Build: 5680278000]", "iOS",
+ "XrAf+Buq6Eb0ehEW/niFp06YX+nGGOS0Ke4MoUBzn+cX9q6G5C0A/d5XtgcNMe8r9jJgV/adIYVpsGS3pVlSAA=="),
+ new ClientVersion("3.8.2 on iOS", "3.8.2 [Build: 1764028963]", "iOS",
+ "Bc57xjumaioWkouTRqoS6XbSIXysrPH+gxynXgL4/nYr38l4OocUTpQaMMHevmgk/FZxJWFNEhU3Hpg5+M+MAA=="),
+ new ClientVersion("3.8.1 on iOS", "3.8.1 [Build: 1763403218]", "iOS",
+ "GM/7isPK6LDKTN3Hdelenbr99dnPyWTccLCOCFKK41321XMsdG02putu+NskCLddlDPu2CPYJ4YABRS2pJ88Ag=="));
+
+ /** The triple used when the user has not chosen one; matches the library's built-in. */
+ public static final ClientVersion DEFAULT = PRESETS.get(0);
+
+ /** The preset carrying exactly these three values, or null when the user typed their own. */
+ public static ClientVersion match(String version, String platform, String sign) {
+ return PRESETS.stream()
+ .filter(v -> v.version.equals(version) && v.platform.equals(platform) && v.sign.equals(sign))
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/ts3-client/core/src/main/java/com/ts3client/config/Settings.java b/ts3-client/core/src/main/java/com/ts3client/config/Settings.java
index 641ee4c..a322b2a 100644
--- a/ts3-client/core/src/main/java/com/ts3client/config/Settings.java
+++ b/ts3-client/core/src/main/java/com/ts3client/config/Settings.java
@@ -51,6 +51,16 @@ public final class Settings {
/** Id of the identity used when a server doesn't select one of its own. */
public String defaultIdentityId = "";
+ // ---- reported client version ----
+ /** Report {@link #clientVersion}/{@link #clientPlatform} instead of the built-in ones. */
+ public boolean customVersion = false;
+ /** Version string other clients see, e.g. "3.5.6 [Build: 1603963200]". */
+ public String clientVersion = ClientVersion.DEFAULT.version();
+ /** Operating system other clients see, e.g. "Windows". */
+ public String clientPlatform = ClientVersion.DEFAULT.platform();
+ /** TeamSpeak's signature over the version and platform; servers may verify it. */
+ public String clientVersionSign = ClientVersion.DEFAULT.sign();
+
// ---- audio devices (mixer names; empty = system default) ----
public String inputDevice = "";
public String outputDevice = "";
@@ -152,6 +162,10 @@ public final class Settings {
serverPassword = props.getProperty("serverPassword", serverPassword);
identityFile = props.getProperty("identityFile", identityFile);
defaultIdentityId = props.getProperty("defaultIdentityId", defaultIdentityId);
+ customVersion = parseB(props.getProperty("customVersion"), customVersion);
+ clientVersion = props.getProperty("clientVersion", clientVersion);
+ clientPlatform = props.getProperty("clientPlatform", clientPlatform);
+ clientVersionSign = props.getProperty("clientVersionSign", clientVersionSign);
inputDevice = props.getProperty("inputDevice", inputDevice);
outputDevice = props.getProperty("outputDevice", outputDevice);
inputMode = parseMode(props.getProperty("inputMode"), inputMode);
@@ -186,6 +200,10 @@ public final class Settings {
props.setProperty("serverPassword", serverPassword);
props.setProperty("identityFile", identityFile);
props.setProperty("defaultIdentityId", defaultIdentityId);
+ props.setProperty("customVersion", Boolean.toString(customVersion));
+ props.setProperty("clientVersion", clientVersion);
+ props.setProperty("clientPlatform", clientPlatform);
+ props.setProperty("clientVersionSign", clientVersionSign);
props.setProperty("inputDevice", inputDevice);
props.setProperty("outputDevice", outputDevice);
props.setProperty("inputMode", inputMode.name());
diff --git a/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java b/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java
index f5d4ca7..40959ed 100644
--- a/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java
+++ b/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java
@@ -226,6 +226,7 @@ public final class TeamspeakConnection implements TS3Listener {
ui.onError("Network error: " + rootMessage(t));
});
+ applyClientVersion();
applyDefaultChannel(channel, channelPassword);
ui.onStatus("Connecting to " + address + ":" + port + "…");
@@ -256,7 +257,7 @@ public final class TeamspeakConnection implements TS3Listener {
applyMicrophoneState();
} catch (Exception e) {
connected = false;
- ui.onError("Connection failed: " + rootMessage(e));
+ ui.onError("Connection failed: " + rootMessage(e) + versionHint(e));
safeCleanup();
// Report the failed attempt as a disconnect too, so the UI leaves the
// "connecting" state and the tab becomes reusable.
@@ -265,6 +266,38 @@ public final class TeamspeakConnection implements TS3Listener {
}
}
+ /**
+ * "client is modified" is what a server answers when it does not accept the reported
+ * version: too old for its minimum, not a version it knows for that platform, or a
+ * signature that does not belong to the two. Easy to run into after editing them by
+ * hand, and the bare message does not point anywhere.
+ */
+ private String versionHint(Exception e) {
+ if (!settings.customVersion) return "";
+ String message = rootMessage(e);
+ return message != null && message.contains("client is modified")
+ ? " — the server did not accept the custom client version; pick a newer one"
+ + " of the listed versions in Options."
+ : "";
+ }
+
+ /**
+ * Reports the version and operating system the user configured, which is what
+ * other clients see in the info panel. Each field falls back to the library's
+ * built-in one when left empty; the signature has to match the version and
+ * platform it was issued for, or a server may refuse the connection.
+ */
+ private void applyClientVersion() {
+ if (!settings.customVersion) return;
+ setIfPresent("client.version_string", settings.clientVersion);
+ setIfPresent("client.version_platform", settings.clientPlatform);
+ setIfPresent("client.version_sign", settings.clientVersionSign);
+ }
+
+ private void setIfPresent(String option, String value) {
+ if (value != null && !value.isBlank()) client.setOption(option, value.trim());
+ }
+
/**
* Asks the server, as part of {@code clientinit}, to place us in a specific
* channel right away instead of the default one. This is the path a real client
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVersionPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVersionPanel.java
new file mode 100644
index 0000000..5073e13
--- /dev/null
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ClientVersionPanel.java
@@ -0,0 +1,190 @@
+package com.ts3client.ui;
+
+import com.ts3client.config.ClientVersion;
+import com.ts3client.config.Settings;
+
+import javax.swing.BorderFactory;
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+
+/**
+ * Options page for the version and operating system reported to the server, which
+ * is what other clients see next to the nickname ("3.6.2 on Windows").
+ *
+ *
Servers verify TeamSpeak's signature over the version and platform and refuse
+ * the connection ("client is modified") when it does not match, so the page is
+ * built around the signed versions in {@link ClientVersion#PRESETS}. The fields
+ * behind them can still be edited by hand for a version whose signature is known
+ * from somewhere else. Changes take effect on the next connect.
+ */
+final class ClientVersionPanel extends JPanel {
+
+ /** Combo entry that leaves the three fields to the user. */
+ private static final String CUSTOM = "Custom…";
+
+ private final Settings settings;
+
+ private final JCheckBox enabled = new JCheckBox("Report a custom version and operating system");
+ private final JComboBox