import _ = require ( 'lodash' ) ; import * as Docker from 'dockerode' ; import * as semver from 'semver' ; import { Mutex } from 'async-mutex' ; import { DOCKER_HOST_HOSTNAME , isImageAvailable } from './docker-commands' ; import { isDockerAvailable } from './docker-interception-services' ; import { delay } from '../../util/promise' ; import { reportError } from '../../error-tracking' ; const DOCKER_TUNNEL_IMAGE = "httptoolkit/docker-socks-tunnel:v1.1.0" ; const DOCKER_TUNNEL_LABEL = "tech.httptoolkit.docker.tunnel" ; const getDockerTunnelContainerName = ( proxyPort ) => `httptoolkit-docker-tunnel-${ proxyPort }` ; const containerMutex = new Mutex ( ) ; export async function prepareDockerTunnel ( ) { if ( ! await isDockerAvailable ( ) ) return ; await containerMutex . runExclusive ( async ( ) => { const docker = new Docker ( ) ; if ( await isImageAvailable ( docker , DOCKER_TUNNEL_IMAGE ) ) return ; else await docker . pull ( DOCKER_TUNNEL_IMAGE ) . catch ( console . warn ) ; } ) ; } const ongoingEnsureTunnelRunningChecks = { } ; export function ensureDockerTunnelRunning ( proxyPort ) { if ( ongoingEnsureTunnelRunningChecks [ proxyPort ] ) { return ongoingEnsureTunnelRunningChecks [ proxyPort ] ! ; } ongoingEnsureTunnelRunningChecks [ proxyPort ] = containerMutex . runExclusive ( async ( ) => { const docker = new Docker ( ) ; if ( ! await docker . getImage ( DOCKER_TUNNEL_IMAGE ) . inspect ( ) . catch ( ( ) => false ) ) { await docker . pull ( DOCKER_TUNNEL_IMAGE ) ; } const containerName = getDockerTunnelContainerName ( proxyPort ) ; let container = await docker . getContainer ( containerName ) . inspect ( ) . catch ( ( ) => undefined ) ; if ( ! container ) { const versionData = await docker . version ( ) ; const engineVersion = semver . coerce ( versionData . Version ) || '0.0.0' ; const defaultBridgeGateway = await docker . listNetworks ( { filters : JSON . stringify ( { driver : [ 'bridge' ] , type : [ 'builtin' ] } ) } ) . then ( ( [ builtinBridge ] ) => builtinBridge ?. IPAM ?. Config ?. [ 0 ] . Gateway ) ; await docker . createContainer ( { name : containerName , Image : DOCKER_TUNNEL_IMAGE , Labels : { [ DOCKER_TUNNEL_LABEL ] : String ( proxyPort ) } , HostConfig : { AutoRemove : true , ... ( process . platform === 'linux' ? { ExtraHosts : [ `${ DOCKER_HOST_HOSTNAME }:${ semver . satisfies ( engineVersion , '>= 20.10' ) ? 'host-gateway' : defaultBridgeGateway || '172.17.0.1' }` ] } : { } ) , PortBindings : { '1080/tcp' : [ { HostIp : '127.0.0.1' } ] } } , } ) ; container = await docker . getContainer ( containerName ) . inspect ( ) ; } if ( ! container . State . Running ) { console . log ( "Starting Docker tunnel..." ) ; await docker . getContainer ( container . Id ) . start ( ) ; } const containerPortMappings = container . NetworkSettings . Ports [ '1080/tcp' ] ; const localTunnelPort = _ . find ( containerPortMappings , ( { HostIp } ) => HostIp === '127.0.0.1' ) ; if ( ! _ . isObject ( portCache [ proxyPort ] ) && localTunnelPort ?. HostPort !== String ( portCache [ proxyPort ] ) ) { const refreshTunnelPort = delay ( 10 ) . then ( ( ) => refreshDockerTunnelPortCache ( proxyPort , { force : true } ) ) ; portCache [ proxyPort ] = refreshTunnelPort ; refreshTunnelPort . catch ( reportError ) ; } } ) . finally ( ( ) => { ongoingEnsureTunnelRunningChecks [ proxyPort ] = undefined ; } ) ; return ongoingEnsureTunnelRunningChecks [ proxyPort ] ! ; } export async function updateDockerTunnelledNetworks ( proxyPort , interceptedNetworks ) { console . log ( `Updating intercepted Docker networks to: ${ interceptedNetworks . join ( ', ' ) }` ) ; const docker = new Docker ( ) ; const defaultBridgeId = docker . listNetworks ( { filters : JSON . stringify ( { driver : [ 'bridge' ] , type : [ 'builtin' ] } ) } ) . then ( ( [ builtinBridge ] ) => builtinBridge ?. Id ) ; const containerName = getDockerTunnelContainerName ( proxyPort ) ; await docker . getContainer ( containerName ) . inspect ( ) . catch ( ( ) => ensureDockerTunnelRunning ( proxyPort ) ) ; await containerMutex . runExclusive ( async ( ) => { const container = await docker . getContainer ( containerName ) . inspect ( ) . catch ( ( ) => undefined ) ; if ( ! container ) { return ; } const expectedNetworks = _ . uniq ( [ ... interceptedNetworks , await defaultBridgeId ] ) ; const currentNetworks = Object . values ( container . NetworkSettings . Networks ) . map ( ( network ) => network . NetworkID ) ; const missingNetworks = expectedNetworks . filter ( ( network ) => ! currentNetworks . includes ( network ) ) ; const extraNetworks = currentNetworks . filter ( ( network ) => ! expectedNetworks . includes ( network ) ) ; await Promise . all ( [ ... missingNetworks . map ( async ( network ) => await docker . getNetwork ( network ) . connect ( { Container : container ! . Id } ) ) , ... extraNetworks . map ( async ( network ) => await docker . getNetwork ( network ) . disconnect ( { Container : container ! . Id } ) ) , ] ) ; } ) ; } const portCache = { } ; export async function getDockerTunnelPort ( proxyPort ) { if ( ! portCache [ proxyPort ] ) { portCache [ proxyPort ] = refreshDockerTunnelPortCache ( proxyPort ) ; } return portCache [ proxyPort ] ! ; } export async function refreshDockerTunnelPortCache ( proxyPort , { force } = { force : false } ) { console . log ( "Querying Docker tunnel port..." ) ; try { if ( ! force && _ . isObject ( portCache [ proxyPort ] ) ) { return portCache [ proxyPort ] ! ; } const docker = new Docker ( ) ; const containerName = getDockerTunnelContainerName ( proxyPort ) ; let container = await docker . getContainer ( containerName ) . inspect ( ) . catch ( ( ) => undefined ) ; if ( ! container ) { await ensureDockerTunnelRunning ( proxyPort ) ; return refreshDockerTunnelPortCache ( proxyPort , { force : true } ) ; } const portMappings = container . NetworkSettings . Ports [ '1080/tcp' ] ; const localPort = _ . find ( portMappings , ( { HostIp } ) => HostIp === '127.0.0.1' ) ; if ( ! localPort ) { await docker . getContainer ( containerName ) . kill ( ) . catch ( ( e ) => { if ( e . statusCode === 404 || e . statusCode === 409 ) return ; else throw e ; } ) ; await ensureDockerTunnelRunning ( proxyPort ) ; await delay ( 10 ) ; return refreshDockerTunnelPortCache ( proxyPort , { force : true } ) ; } const port = parseInt ( localPort . HostPort , 10 ) ; portCache [ proxyPort ] = port ; return port ; } catch ( e ) { portCache [ proxyPort ] = undefined ; throw e ; } } export async function stopDockerTunnel ( proxyPort ) { const docker = new Docker ( ) ; await containerMutex . runExclusive ( async ( ) => { const containers = await docker . listContainers ( { all : true , filters : JSON . stringify ( { label : [ proxyPort === 'all' ? DOCKER_TUNNEL_LABEL : `${ DOCKER_TUNNEL_LABEL }=${ proxyPort }` ] } ) } ) ; await Promise . all ( containers . map ( async ( containerData ) => { const container = docker . getContainer ( containerData . Id ) ; await container . remove ( { force : true } ) . catch ( ( ) => { } ) ; } ) ) ; } ) ; }