Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions app/socket_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1547,7 +1547,7 @@ def request_auth_banner_decision(banner, context):
if ((existing.socks_proxy_id or existing.via_socks or saved_socks_proxy_id)
and (not existing.socks_proxy_id
or existing.socks_proxy_id != saved_socks_proxy_id)):
emit_error('Saved SOCKS5 proxy route no longer matches this session')
emit_error('This persistent session uses a different SOCKS5 route. Close it and connect again.')
return
reconnect_tmux_name = raw_name

Expand Down Expand Up @@ -2533,10 +2533,9 @@ def handle_delete_jump_host(data, current_user=None):
@socketio.on('list_socks_proxies')
@socket_login_required
def handle_list_socks_proxies(current_user=None):
# Reads stay outside the mutation budget, as for jump hosts: a throttled list
# would show every routed host as having a missing proxy.
try:
limited = _connection_mutation_rate_limit(current_user)
if limited:
return limited
emit('socks_proxies_list', {
'socks_proxies': socks_proxy_manager.load_socks_proxies(current_user.id),
})
Expand Down Expand Up @@ -2586,6 +2585,21 @@ def handle_delete_socks_proxy(data, current_user=None):
proxy_id = data.get('socks_proxy_id')
if not isinstance(proxy_id, str) or not proxy_id or len(proxy_id) > 64:
return _command_set_error('SOCKS proxy ID required')
# Reattaching a persistent tmux session requires its original proxy, so
# deleting that proxy would strand the session.
persistent = SSHSession.query.filter_by(
user_id=current_user.id, is_persistent=True, socks_proxy_id=proxy_id,
).order_by(SSHSession.id).all()
if persistent:
labels = [
session.display_name or f'{session.username}@{session.host}'
for session in persistent[:20]
]
noun = 'session' if len(persistent) == 1 else 'sessions'
return _command_set_error(
f'SOCKS proxy is used by {len(persistent)} persistent {noun}',
labels,
)
success, error, usages = socks_proxy_manager.delete_socks_proxy(current_user.id, proxy_id)
if not success:
return _command_set_error(error, usages)
Expand Down
6 changes: 4 additions & 2 deletions app/socks5_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ def open_socks5_tunnel(sock, destination, *, username=None, password=None,
version, reply, reserved, address_type = _recv_exact(sock, 4, deadline, cancelled)
if version != 5 or reserved != 0:
raise Socks5Error('socks_protocol_error')
# The socket is closed on failure, so a failed reply needs no bound
# address; proxies that truncate it still report the real reason.
if reply != 0:
raise Socks5Error(_REPLY_CODES.get(reply, 'socks_protocol_error'))
if address_type == 1:
address_length = 4
elif address_type == 4:
Expand All @@ -186,8 +190,6 @@ def open_socks5_tunnel(sock, destination, *, username=None, password=None,
raise Socks5Error('socks_protocol_error')
_recv_exact(sock, address_length, deadline, cancelled)
_recv_exact(sock, 2, deadline, cancelled)
if reply != 0:
raise Socks5Error(_REPLY_CODES.get(reply, 'socks_protocol_error'))
_remaining(deadline, cancelled)
succeeded = True
except Socks5Error as error:
Expand Down
13 changes: 8 additions & 5 deletions app/ssh_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,11 +428,14 @@ def connection_cancelled():
remote_dns_allowlist=config.SOCKS_REMOTE_DNS_ALLOWLIST,
)
host, port = destination.hostname, destination.port
proxy_target = resolve_allowed_target(
socks_proxy.host, socks_proxy.port,
allow_internal=not config.BLOCK_INTERNAL_SSH,
allowed_internal_networks=config.SSH_ALLOWED_INTERNAL_NETWORKS,
)
try:
proxy_target = resolve_allowed_target(
socks_proxy.host, socks_proxy.port,
allow_internal=not config.BLOCK_INTERNAL_SSH,
allowed_internal_networks=config.SSH_ALLOWED_INTERNAL_NETWORKS,
)
except ValueError as error:
raise ValueError(f'SOCKS5 proxy: {error}') from error
audit_internal_allowlist_use(proxy_target, 'socks_proxy')
try:
validated_socket = open_validated_socket(proxy_target, config.SSH_CONNECT_TIMEOUT)
Expand Down
8 changes: 8 additions & 0 deletions docs/wiki/Data-Storage-and-Persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ The additive migration leaves existing rows without a proxy and sets
JSON migrations from unversioned data through versions 1 and 2 only add or advance
the schema marker.

Releases without SOCKS5 support keep an unknown `socks_proxy_id` in profiles but
ignore it, so a downgraded instance connects those hosts directly. Saved
passwords stay unusable there because their route binding no longer matches, but
SSH keys and passwords entered at connect time would reach the target without
the proxy. Before downgrading, remove the SOCKS5 route from every host in the
updated application, or restore the complete pre-upgrade backup with its
matching secret into a separate directory.

### Workspace upgrade and rollback

The workspace update adds a nullable `profile_id` column to saved SSH sessions.
Expand Down
4 changes: 3 additions & 1 deletion docs/wiki/Profiles-Jump-Hosts-and-Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ review, and a failed proxy never causes a direct connection attempt.
**Edit** keeps the proxy's ID; dependent hosts use its current configuration on
their next connection. Existing sessions are not restarted. **Delete** requires
confirmation and is blocked while saved hosts reference the proxy; the error
lists up to 20 affected host names and the total count. If a save times out,
lists up to 20 affected host names and the total count. It is also blocked while
a persistent tmux session was opened through the proxy, because that session can
only be resumed over its original route; close the session first. If a save times out,
check the saved list before retrying because the write may already have succeeded.

SOCKS5 routes are unsupported in standalone **Quick SFTP** and Automation runs
Expand Down
17 changes: 16 additions & 1 deletion static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@
};

function openConnectionAssetManager(asset) {
// Resource views open behind the connection dialog, so leave it first.
if (document.getElementById('connectionModal')?.classList.contains('show')) {
dismissConnectionModal();
}
if (asset === 'passwords') {
window.primaryWorkspaceController?.open('hosts', document.getElementById('credentialManagementView'));
window.NativeCredentials?.mount();
Expand All @@ -303,7 +307,7 @@
ProfileManager.loadKeys();
if (asset === 'jump-hosts') {
window.JumpHostManager?.load();
window.SocksProxyManager?.load();
window.SocksProxyManager?.load();
window.JumpHostManager?.renderList();
}
const view = document.getElementById(modalId);
Expand Down Expand Up @@ -432,6 +436,11 @@
const unresolved = !proxy || Boolean(session.jumpHostId || session.viaJump) || session.reconnectRouteKnown === false;
if (applyToForm) {
selectedConnectionProfileState = {requiredSocksProxyId: session.socksProxyId || '', missingSocksProxy: unresolved};
const jumpSelect = document.getElementById('jumpHostSelect');
if (jumpSelect) jumpSelect.value = '';
const jumpPassword = document.getElementById('jumpHostPasswordInput');
if (jumpPassword) jumpPassword.value = '';
window.JumpHostManager?.updatePasswordVisibility();
manager?.selectReference(document.getElementById('socksProxySelect'), session.socksProxyId);
const password = document.getElementById('socksProxyPasswordInput');
if (password) password.value = '';
Expand All @@ -450,6 +459,10 @@
|| session.reconnectRouteKnown === false);
if (applyToForm) {
selectedConnectionProfileState = {missingJumpHost: unresolved};
const socksSelect = document.getElementById('socksProxySelect');
if (socksSelect) socksSelect.value = '';
const socksPassword = document.getElementById('socksProxyPasswordInput');
if (socksPassword) socksPassword.value = '';
const select = document.getElementById('jumpHostSelect');
if (select) select.value = jump ? requiredId : '';
// Setting the value in code fires no change event; apply the jump host's
Expand All @@ -460,6 +473,8 @@
window.CredentialPicker?.restore?.('jumpHostPasswordInput');
}
manager?.updatePasswordVisibility();
window.SocksProxyManager?.updatePasswordVisibility();
window.SocksProxyManager?.updateRouteControls();
window.setConnectionAdvancedExpanded(Boolean(jump || unresolved));
refreshConnectionProfileJumpResolution();
if (jump?.auth_type === 'password') {
Expand Down
24 changes: 12 additions & 12 deletions static/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ const translations = {
"socksproxies.deleted": "SOCKS5 proxy deleted",
"socksproxies.confirmDelete": "Delete this SOCKS5 proxy?",
"socksproxies.inUse": "This SOCKS5 proxy is used by {count} host(s)",
"socksproxies.inUseSessions": "This SOCKS5 proxy is still used by {count} persistent session(s). Close them before deleting the proxy",
"socksproxies.saveUnknownHint": "Save result unknown. Check the saved list before trying again. Your draft is preserved.",
"socksproxies.saveFailed": "Could not save the SOCKS5 proxy.",
"socksproxies.loading": "Loading SOCKS5 proxies…",
"connection.route": "Route",
"connection.routeClearJump": "A host uses one route: clear the jump host to choose a SOCKS5 proxy.",
Expand All @@ -35,7 +35,7 @@ const translations = {
"connection.routeConflict": "Choose one compatible route.",
"connection.socksProxy": "SOCKS5 Proxy",
"connection.socksProxyNone": "None",
"connection.socksProxyManage": "Manage SOCKS5 proxies in the connection resources.",
"connection.socksProxyManage": "Manage SOCKS5 proxies",
"connection.socksProxyPassword": "SOCKS5 Proxy Password",
"connection.socksProxyPasswordHint": "This proxy uses password login. Enter its password; it is not stored unless you save it.",
"connection.socksProxyPasswordRequired": "SOCKS5 proxy password is required",
Expand Down Expand Up @@ -2521,8 +2521,8 @@ const translations = {
"socksproxies.deleted": "Đã xóa proxy SOCKS5",
"socksproxies.confirmDelete": "Xóa proxy SOCKS5 này?",
"socksproxies.inUse": "Proxy SOCKS5 này đang được {count} máy chủ sử dụng",
"socksproxies.inUseSessions": "Proxy SOCKS5 này vẫn đang được {count} phiên lâu dài sử dụng. Hãy đóng các phiên đó trước khi xóa proxy",
"socksproxies.saveUnknownHint": "Chưa rõ kết quả lưu. Kiểm tra danh sách đã lưu trước khi thử lại. Bản nháp vẫn được giữ.",
"socksproxies.saveFailed": "Không thể lưu proxy SOCKS5.",
"socksproxies.loading": "Đang tải proxy SOCKS5…",
"connection.route": "Tuyến kết nối",
"connection.routeClearJump": "Máy chủ chỉ dùng một tuyến: bỏ máy chủ trung chuyển để chọn proxy SOCKS5.",
Expand All @@ -2531,7 +2531,7 @@ const translations = {
"connection.routeConflict": "Chọn một tuyến kết nối tương thích.",
"connection.socksProxy": "Proxy SOCKS5",
"connection.socksProxyNone": "Không có",
"connection.socksProxyManage": "Quản lý proxy SOCKS5 trong tài nguyên kết nối.",
"connection.socksProxyManage": "Quản lý proxy SOCKS5",
"connection.socksProxyPassword": "Mật khẩu proxy SOCKS5",
"connection.socksProxyPasswordHint": "Proxy này dùng mật khẩu để đăng nhập. Hãy nhập mật khẩu; mật khẩu chỉ được lưu khi bạn yêu cầu.",
"connection.socksProxyPasswordRequired": "Cần nhập mật khẩu proxy SOCKS5",
Expand Down Expand Up @@ -5011,8 +5011,8 @@ const translations = {
"socksproxies.deleted": "SOCKS5-Proxy gelöscht",
"socksproxies.confirmDelete": "Diesen SOCKS5-Proxy löschen?",
"socksproxies.inUse": "Dieser SOCKS5-Proxy wird von {count} Host(s) verwendet",
"socksproxies.inUseSessions": "Dieser SOCKS5-Proxy wird noch von {count} persistenten Session(s) verwendet. Schließe sie, bevor du den Proxy löschst",
"socksproxies.saveUnknownHint": "Speicherergebnis unbekannt. Prüfe vor einem weiteren Versuch die gespeicherte Liste. Dein Entwurf bleibt erhalten.",
"socksproxies.saveFailed": "SOCKS5-Proxy konnte nicht gespeichert werden.",
"socksproxies.loading": "SOCKS5-Proxys werden geladen…",
"connection.route": "Route",
"connection.routeClearJump": "Ein Host verwendet eine Route: Entferne den Jump Host, um einen SOCKS5-Proxy zu wählen.",
Expand All @@ -5021,7 +5021,7 @@ const translations = {
"connection.routeConflict": "Wähle eine kompatible Route.",
"connection.socksProxy": "SOCKS5-Proxy",
"connection.socksProxyNone": "Keiner",
"connection.socksProxyManage": "SOCKS5-Proxys in den Verbindungsressourcen verwalten.",
"connection.socksProxyManage": "SOCKS5-Proxys verwalten",
"connection.socksProxyPassword": "SOCKS5-Proxy-Passwort",
"connection.socksProxyPasswordHint": "Dieser Proxy verwendet eine Passwort-Anmeldung. Gib sein Passwort ein; es wird nur auf ausdrücklichen Wunsch gespeichert.",
"connection.socksProxyPasswordRequired": "SOCKS5-Proxy-Passwort erforderlich",
Expand Down Expand Up @@ -7505,8 +7505,8 @@ const translations = {
"socksproxies.deleted": "Proxy SOCKS5 supprimé",
"socksproxies.confirmDelete": "Supprimer ce proxy SOCKS5 ?",
"socksproxies.inUse": "Ce proxy SOCKS5 est utilisé par {count} hôte(s)",
"socksproxies.inUseSessions": "Ce proxy SOCKS5 est encore utilisé par {count} session(s) persistante(s). Fermez-les avant de supprimer le proxy",
"socksproxies.saveUnknownHint": "Résultat inconnu. Vérifiez la liste avant de réessayer. Votre brouillon est conservé.",
"socksproxies.saveFailed": "Impossible d’enregistrer le proxy SOCKS5.",
"socksproxies.loading": "Chargement des proxys SOCKS5…",
"connection.route": "Route",
"connection.routeClearJump": "Un hôte utilise une seule route : retirez l’hôte de rebond pour choisir un proxy SOCKS5.",
Expand All @@ -7515,7 +7515,7 @@ const translations = {
"connection.routeConflict": "Choisissez une seule route compatible.",
"connection.socksProxy": "Proxy SOCKS5",
"connection.socksProxyNone": "Aucun",
"connection.socksProxyManage": "Gérer les proxys SOCKS5 dans les ressources de connexion.",
"connection.socksProxyManage": "Gérer les proxys SOCKS5",
"connection.socksProxyPassword": "Mot de passe du proxy SOCKS5",
"connection.socksProxyPasswordHint": "Ce proxy utilise un mot de passe. Saisissez-le ; il n’est conservé que si vous l’enregistrez.",
"connection.socksProxyPasswordRequired": "Le mot de passe du proxy SOCKS5 est requis",
Expand Down Expand Up @@ -9995,8 +9995,8 @@ const translations = {
"socksproxies.deleted": "Proxy SOCKS5 eliminado",
"socksproxies.confirmDelete": "¿Eliminar este proxy SOCKS5?",
"socksproxies.inUse": "Este proxy SOCKS5 lo usan {count} hosts",
"socksproxies.inUseSessions": "Este proxy SOCKS5 todavía lo usan {count} sesiones persistentes. Ciérralas antes de eliminar el proxy",
"socksproxies.saveUnknownHint": "Resultado desconocido. Revisa la lista antes de reintentarlo. Se conserva tu borrador.",
"socksproxies.saveFailed": "No se pudo guardar el proxy SOCKS5.",
"socksproxies.loading": "Cargando proxies SOCKS5…",
"connection.route": "Ruta",
"connection.routeClearJump": "Un host usa una sola ruta: quita el host de salto para elegir un proxy SOCKS5.",
Expand All @@ -10005,7 +10005,7 @@ const translations = {
"connection.routeConflict": "Elige una sola ruta compatible.",
"connection.socksProxy": "Proxy SOCKS5",
"connection.socksProxyNone": "Ninguno",
"connection.socksProxyManage": "Administra los proxies SOCKS5 en los recursos de conexión.",
"connection.socksProxyManage": "Administrar proxies SOCKS5",
"connection.socksProxyPassword": "Contraseña del proxy SOCKS5",
"connection.socksProxyPasswordHint": "Este proxy usa contraseña. Introdúcela; solo se almacena si la guardas.",
"connection.socksProxyPasswordRequired": "Se requiere la contraseña del proxy SOCKS5",
Expand Down Expand Up @@ -12485,8 +12485,8 @@ const translations = {
"socksproxies.deleted": "SOCKS5 代理已删除",
"socksproxies.confirmDelete": "删除此 SOCKS5 代理?",
"socksproxies.inUse": "此 SOCKS5 代理正被 {count} 台主机使用",
"socksproxies.inUseSessions": "此 SOCKS5 代理仍被 {count} 个持久会话使用。请先关闭这些会话再删除代理",
"socksproxies.saveUnknownHint": "保存结果未知。再次尝试前请检查已保存列表。草稿已保留。",
"socksproxies.saveFailed": "无法保存 SOCKS5 代理。",
"socksproxies.loading": "正在加载 SOCKS5 代理…",
"connection.route": "连接路由",
"connection.routeClearJump": "一台主机只能使用一条路由:清除跳板机后才能选择 SOCKS5 代理。",
Expand All @@ -12495,7 +12495,7 @@ const translations = {
"connection.routeConflict": "请选择一条兼容的连接路由。",
"connection.socksProxy": "SOCKS5 代理",
"connection.socksProxyNone": "无",
"connection.socksProxyManage": "在连接资源中管理 SOCKS5 代理。",
"connection.socksProxyManage": "管理 SOCKS5 代理",
"connection.socksProxyPassword": "SOCKS5 代理密码",
"connection.socksProxyPasswordHint": "此代理使用密码登录。请输入密码;只有明确保存时才会存储。",
"connection.socksProxyPasswordRequired": "请输入 SOCKS5 代理密码",
Expand Down
10 changes: 5 additions & 5 deletions static/js/socks-proxy-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,6 @@ window.SocksProxyManager = {
this.unknownOutcome = true;
window.showNotification?.(this.t('common.saveUnknownTitle', 'Save outcome unknown'), 'error');
this.load();
} else if (!result?.success) {
window.showNotification?.(result?.error || this.t('socksproxies.saveFailed', 'Could not save the SOCKS5 proxy.'), 'error');
} else if (result?.success) {
if (!current()) return;
this.upsertSocksProxy(result.socks_proxy);
Expand All @@ -298,9 +296,11 @@ window.SocksProxyManager = {
},

handleUsageError(data) {
const count = /^SOCKS proxy is used by (\d+)/.exec(data?.error || '')?.[1];
if (!count || !Array.isArray(data.usages)) return false;
const message = this.t('socksproxies.inUse', 'This SOCKS5 proxy is used by {count} host(s)').replace('{count}', count);
const match = /^SOCKS proxy is used by (\d+)( persistent)?/.exec(data?.error || '');
if (!match || !Array.isArray(data.usages)) return false;
const message = (match[2]
? this.t('socksproxies.inUseSessions', 'This SOCKS5 proxy is still used by {count} persistent session(s). Close them before deleting the proxy')
: this.t('socksproxies.inUse', 'This SOCKS5 proxy is used by {count} host(s)')).replace('{count}', match[1]);
window.showNotification?.(message + ': ' + data.usages.slice(0, 20).join(', '), 'error');
return true;
},
Expand Down
Loading
Loading