diff --git a/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index 29b07680481..1c0d70f63ff 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/src/vs/platform/tunnel/common/tunnel.ts @@ -129,6 +129,7 @@ export interface ITunnelService { closeTunnel(remoteHost: string, remotePort: number): Promise; setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable; setTunnelFeatures(features: TunnelProviderFeatures): void; + isPortPrivileged(port: number): boolean; } export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string; port: number } | undefined { @@ -402,6 +403,24 @@ export abstract class AbstractTunnelService implements ITunnelService { return !!extractLocalHostUriMetaDataForPortMapping(uri); } + public abstract isPortPrivileged(port: number): boolean; + + protected doIsPortPrivileged(port: number, isWindows: boolean, isMacintosh: boolean, osRelease: string): boolean { + if (isWindows) { + return false; + } else if (isMacintosh) { + const osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(osRelease); + if (osVersion?.length === 4) { + const major = parseInt(osVersion[1]); + const minor = parseInt(osVersion[2]); + if (((major > 10) || (major === 10 && minor >= 14))) { + return false; + } + } + } + return port < 1024; + } + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined; protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { @@ -409,7 +428,7 @@ export abstract class AbstractTunnelService implements ITunnelService { const key = remotePort; this._factoryInProgress.add(key); const preferredLocalPort = localPort === undefined ? remotePort : localPort; - const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false }; + const creationInfo = { elevationRequired: elevateIfNeeded ? this.isPortPrivileged(preferredLocalPort) : false }; const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, privacy, public: privacy ? (privacy !== TunnelPrivacyId.Private) : undefined, protocol }; const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo); if (tunnel) { diff --git a/src/vs/platform/tunnel/node/tunnelService.ts b/src/vs/platform/tunnel/node/tunnelService.ts index 7cdd91afd53..50a7cb920af 100644 --- a/src/vs/platform/tunnel/node/tunnelService.ts +++ b/src/vs/platform/tunnel/node/tunnelService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as net from 'net'; +import * as os from 'os'; import { BROWSER_RESTRICTED_PORTS, findFreePortFaster } from 'vs/base/node/ports'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; @@ -16,6 +17,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { connectRemoteAgentTunnel, IAddressProvider, IConnectionOptions, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { AbstractTunnelService, isAllInterfaces, ISharedTunnelsService as ISharedTunnelsService, isLocalhost, ITunnelService, RemoteTunnel, TunnelPrivacyId } from 'vs/platform/tunnel/common/tunnel'; import { ISignService } from 'vs/platform/sign/common/sign'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; async function createRemoteTunnel(options: IConnectionOptions, defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { let readyTunnel: NodeRemoteTunnel | undefined; @@ -165,6 +167,10 @@ export class BaseTunnelService extends AbstractTunnelService { return (!settingValue || settingValue === 'localhost') ? '127.0.0.1' : '0.0.0.0'; } + public isPortPrivileged(port: number): boolean { + return this.doIsPortPrivileged(port, isWindows, isMacintosh, os.release()); + } + protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index c43a9e82756..ea4f8198698 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -8,7 +8,7 @@ import { MainThreadTunnelServiceShape, MainContext, ExtHostContext, ExtHostTunne import { TunnelDtoConverter } from 'vs/workbench/api/common/extHostTunnelService'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { CandidatePort, IRemoteExplorerService, makeAddress, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT, PORT_AUTO_SOURCE_SETTING_PROCESS, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService'; -import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, isPortPrivileged, ProvidedPortAttributes, PortAttributesProvider, TunnelProtocol } from 'vs/platform/tunnel/common/tunnel'; +import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, ProvidedPortAttributes, PortAttributesProvider, TunnelProtocol } from 'vs/platform/tunnel/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -111,7 +111,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun if (!this.elevateionRetry && (tunnelOptions.localAddressPort !== undefined) && (tunnel.tunnelLocalPort !== undefined) - && isPortPrivileged(tunnelOptions.localAddressPort) + && this.tunnelService.isPortPrivileged(tunnelOptions.localAddressPort) && (tunnel.tunnelLocalPort !== tunnelOptions.localAddressPort) && this.tunnelService.canElevate) { diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index e9f2f1ae804..15e4776d4a9 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -339,7 +339,7 @@ class OnAutoForwardedAction extends Disposable { choices.push(this.openPreviewChoice(tunnel)); } - if ((tunnel.tunnelLocalPort !== tunnel.tunnelRemotePort) && this.tunnelService.canElevate && isPortPrivileged(tunnel.tunnelRemotePort)) { + if ((tunnel.tunnelLocalPort !== tunnel.tunnelRemotePort) && this.tunnelService.canElevate && this.tunnelService.isPortPrivileged(tunnel.tunnelRemotePort)) { // Privileged ports are not on Windows, so it's safe to use "superuser" message += nls.localize('remote.tunnelsView.elevationMessage', "You'll need to run as superuser to use port {0} locally. ", tunnel.tunnelRemotePort); choices.unshift(this.elevateChoice(tunnel)); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 09222cf1888..d598e2d2ece 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -35,7 +35,7 @@ import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platfor import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { URI } from 'vs/base/common/uri'; -import { isAllInterfaces, isLocalhost, isPortPrivileged, ITunnelService, RemoteTunnel, TunnelPrivacyId, TunnelProtocol } from 'vs/platform/tunnel/common/tunnel'; +import { isAllInterfaces, isLocalhost, ITunnelService, RemoteTunnel, TunnelPrivacyId, TunnelProtocol } from 'vs/platform/tunnel/common/tunnel'; import { TunnelPrivacy } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -1138,13 +1138,13 @@ export namespace ForwardPortAction { export const TREEITEM_LABEL = nls.localize('remote.tunnel.forwardItem', "Forward Port"); const forwardPrompt = nls.localize('remote.tunnel.forwardPrompt', "Port number or address (eg. 3000 or 10.10.10.10:2000)."); - function validateInput(remoteExplorerService: IRemoteExplorerService, value: string, canElevate: boolean): { content: string; severity: Severity } | null { + function validateInput(remoteExplorerService: IRemoteExplorerService, tunnelService: ITunnelService, value: string, canElevate: boolean): { content: string; severity: Severity } | null { const parsed = parseAddress(value); if (!parsed) { return { content: invalidPortString, severity: Severity.Error }; } else if (parsed.port >= maxPortNumber) { return { content: invalidPortNumberString, severity: Severity.Error }; - } else if (canElevate && isPortPrivileged(parsed.port)) { + } else if (canElevate && tunnelService.isPortPrivileged(parsed.port)) { return { content: requiresSudoString, severity: Severity.Info }; } else if (mapHasAddressLocalhostOrAllInterfaces(remoteExplorerService.tunnelModel.forwarded, parsed.host, parsed.port)) { return { content: alreadyForwarded, severity: Severity.Error }; @@ -1174,7 +1174,7 @@ export namespace ForwardPortAction { }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port)); } }, - validationMessage: (value) => validateInput(remoteExplorerService, value, tunnelService.canElevate), + validationMessage: (value) => validateInput(remoteExplorerService, tunnelService, value, tunnelService.canElevate), placeholder: forwardPrompt }); }; @@ -1190,7 +1190,7 @@ export namespace ForwardPortAction { await viewsService.openView(TunnelPanel.ID, true); const value = await quickInputService.input({ prompt: forwardPrompt, - validateInput: (value) => Promise.resolve(validateInput(remoteExplorerService, value, tunnelService.canElevate)) + validateInput: (value) => Promise.resolve(validateInput(remoteExplorerService, tunnelService, value, tunnelService.canElevate)) }); let parsed: { host: string; port: number } | undefined; if (value && (parsed = parseAddress(value))) { @@ -1436,12 +1436,12 @@ namespace ChangeLocalPortAction { export const ID = 'remote.tunnel.changeLocalPort'; export const LABEL = nls.localize('remote.tunnel.changeLocalPort', "Change Local Address Port"); - function validateInput(value: string, canElevate: boolean): { content: string; severity: Severity } | null { + function validateInput(tunnelService: ITunnelService, value: string, canElevate: boolean): { content: string; severity: Severity } | null { if (!value.match(/^[0-9]+$/)) { return { content: invalidPortString, severity: Severity.Error }; } else if (Number(value) >= maxPortNumber) { return { content: invalidPortNumberString, severity: Severity.Error }; - } else if (canElevate && isPortPrivileged(Number(value))) { + } else if (canElevate && tunnelService.isPortPrivileged(Number(value))) { return { content: requiresSudoString, severity: Severity.Info }; } return null; @@ -1484,7 +1484,7 @@ namespace ChangeLocalPortAction { } } }, - validationMessage: (value) => validateInput(value, tunnelService.canElevate), + validationMessage: (value) => validateInput(tunnelService, value, tunnelService.canElevate), placeholder: nls.localize('remote.tunnelsView.changePort', "New local port") }); } diff --git a/src/vs/workbench/services/tunnel/browser/tunnelService.ts b/src/vs/workbench/services/tunnel/browser/tunnelService.ts index 9bebab0ada4..179594037d5 100644 --- a/src/vs/workbench/services/tunnel/browser/tunnelService.ts +++ b/src/vs/workbench/services/tunnel/browser/tunnelService.ts @@ -18,6 +18,10 @@ export class TunnelService extends AbstractTunnelService { super(logService); } + public isPortPrivileged(_port: number): boolean { + return false; + } + protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { diff --git a/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts b/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts index eb6835f334d..dcc855a7e9d 100644 --- a/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts +++ b/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts @@ -14,6 +14,8 @@ import { ISharedProcessTunnelService } from 'vs/platform/remote/common/sharedPro import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; class SharedProcessTunnel extends Disposable implements RemoteTunnel { @@ -59,6 +61,7 @@ export class TunnelService extends AbstractTunnelService { @ISharedProcessTunnelService private readonly _sharedProcessTunnelService: ISharedProcessTunnelService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILifecycleService lifecycleService: ILifecycleService, + @INativeWorkbenchEnvironmentService private readonly _nativeWorkbenchEnvironmentService: INativeWorkbenchEnvironmentService ) { super(logService); @@ -70,6 +73,10 @@ export class TunnelService extends AbstractTunnelService { }); } + public isPortPrivileged(port: number): boolean { + return this.doIsPortPrivileged(port, isWindows, isMacintosh, this._nativeWorkbenchEnvironmentService.os.release); + } + protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) {