From ce978dedce297110eaa2963fff3c7287e725c69c Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 11 May 2026 01:47:24 +0800 Subject: [PATCH] Support IBKR mobile push 2FA config --- .github/workflows/main.yml | 4 ++++ 2fa_bot.py | 19 +++++++++++++++++++ README.md | 10 ++++++++++ docker-compose.yml | 4 ++++ tests/test_docker_compose_ports.sh | 2 ++ tests/test_workflow_shared_config.sh | 4 ++++ 6 files changed, 43 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 66d3cce..74d0475 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,6 +53,8 @@ jobs: ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY: ${{ vars.IB_GATEWAY_ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY }} TWS_ACCEPT_INCOMING: ${{ vars.IB_GATEWAY_TWS_ACCEPT_INCOMING }} READ_ONLY_API: ${{ vars.IB_GATEWAY_READ_ONLY_API }} + TWOFA_DEVICE: ${{ vars.IB_GATEWAY_TWOFA_DEVICE }} + IBKR_2FA_AUTOFILL: ${{ vars.IB_GATEWAY_2FA_AUTOFILL }} SSH_PRIVATE_KEY_SECRET_NAME: ${{ vars.IB_GATEWAY_SSH_PRIVATE_KEY_SECRET_NAME }} TWS_USERID_SECRET_NAME: ${{ vars.IB_GATEWAY_TWS_USERID_SECRET_NAME }} TWS_PASSWORD_SECRET_NAME: ${{ vars.IB_GATEWAY_TWS_PASSWORD_SECRET_NAME }} @@ -232,6 +234,8 @@ jobs: "ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY": os.environ["ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY"], "TWS_ACCEPT_INCOMING": os.environ["TWS_ACCEPT_INCOMING"], "READ_ONLY_API": os.environ["READ_ONLY_API"], + "TWOFA_DEVICE": os.environ.get("TWOFA_DEVICE", ""), + "IBKR_2FA_AUTOFILL": os.environ.get("IBKR_2FA_AUTOFILL", ""), } def quote(value: str) -> str: diff --git a/2fa_bot.py b/2fa_bot.py index 0835e4c..c7d25c8 100644 --- a/2fa_bot.py +++ b/2fa_bot.py @@ -11,6 +11,12 @@ # ================= Configuration ================= SECRET_KEY = os.environ.get("TOTP_SECRET") X11_DISPLAY = os.environ.get("DISPLAY_NUM", ":1") +AUTOFILL_ENABLED = os.environ.get("IBKR_2FA_AUTOFILL", "yes").strip().lower() not in { + "0", + "false", + "no", + "off", +} # Timing constants (seconds) CHECK_INTERVAL = 3 @@ -72,6 +78,9 @@ class WindowCandidate: def validate_config(): """Validate required config at startup; exit immediately if invalid.""" + if not AUTOFILL_ENABLED: + log.info("TOTP auto-fill is disabled; bot will only log auth popup detection") + return if not SECRET_KEY: log.error("TOTP_SECRET not found in environment variables") sys.exit(1) @@ -188,6 +197,16 @@ def focus_input_area(candidate): def submit_totp(candidate): """Submit a TOTP code to the selected authentication popup.""" + if not AUTOFILL_ENABLED: + log.info( + "Authentication window found (id=%s, title=%r, size=%sx%s); auto-fill disabled", + candidate.window_id, + candidate.title, + candidate.width or "?", + candidate.height or "?", + ) + return + seconds_remaining = wait_for_fresh_totp_window() log.info( "Authentication window found (id=%s, title=%r, size=%sx%s); submitting code with %ss remaining", diff --git a/README.md b/README.md index 8226687..9121747 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,11 @@ VNC_SERVER_PASSWORD=your_vnc_password TRADING_MODE=live TWS_ACCEPT_INCOMING=accept READ_ONLY_API=no +TWOFA_DEVICE=Mobile Device TWOFA_TIMEOUT_ACTION=restart RELOGIN_AFTER_TWOFA_TIMEOUT=yes EXISTING_SESSION_DETECTED_ACTION=primary +IBKR_2FA_AUTOFILL=no JAVA_HEAP_SIZE=512 # Recommended: use the exact CIDR used by your Cloud Run egress path @@ -94,6 +96,8 @@ IB_GATEWAY_DEPLOY_PATH=/home/zwlddx0815/ib-docker IB_GATEWAY_ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY=no IB_GATEWAY_TWS_ACCEPT_INCOMING=accept IB_GATEWAY_READ_ONLY_API=no +IB_GATEWAY_TWOFA_DEVICE=Mobile Device +IB_GATEWAY_2FA_AUTOFILL=no ``` The workflow maps these shared values to the gateway container's `.env`: @@ -104,6 +108,8 @@ The workflow maps these shared values to the gateway container's `.env`: - `IB_GATEWAY_ZONE` -> `GCE_ZONE` - `IB_GATEWAY_GCE_USER` -> `GCE_USER` - `IB_GATEWAY_DEPLOY_PATH` -> `DEPLOY_PATH` +- `IB_GATEWAY_TWOFA_DEVICE` -> `TWOFA_DEVICE` +- `IB_GATEWAY_2FA_AUTOFILL` -> `IBKR_2FA_AUTOFILL` `ACCEPT_API_FROM_IP` is intentionally treated as required now. For manual `docker compose` usage, if you forget to set it, Compose will fail fast instead of starting a gateway that Cloud Run can never reach. @@ -205,6 +211,8 @@ If you temporarily keep the values in GitHub Secrets during migration, you can r | `IB_GATEWAY_ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY` | Set to `no` for Cloud Run private IP access | | `IB_GATEWAY_TWS_ACCEPT_INCOMING` | Optional. Recommended `accept`. | | `IB_GATEWAY_READ_ONLY_API` | Optional. Recommended `no` if this service places trades. | +| `IB_GATEWAY_TWOFA_DEVICE` | Optional. Exact IBC 2FA device name, for example `Mobile Device` for IBKR Mobile push. | +| `IB_GATEWAY_2FA_AUTOFILL` | Optional. Set to `no` when using IBKR Mobile push instead of local TOTP auto-fill. | The current VM is an `e2-micro`, so the deployment intentionally sets `JAVA_HEAP_SIZE=512` by default and enables a 2 GiB host swap file during keepalive/deploy. Without this, @@ -358,9 +366,11 @@ VNC_SERVER_PASSWORD=your_vnc_password TRADING_MODE=live TWS_ACCEPT_INCOMING=accept READ_ONLY_API=no +TWOFA_DEVICE=Mobile Device TWOFA_TIMEOUT_ACTION=restart RELOGIN_AFTER_TWOFA_TIMEOUT=yes EXISTING_SESSION_DETECTED_ACTION=primary +IBKR_2FA_AUTOFILL=no JAVA_HEAP_SIZE=512 ACCEPT_API_FROM_IP=10.8.0.0/26 diff --git a/docker-compose.yml b/docker-compose.yml index 73a60c7..e01cfa7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,9 +13,13 @@ services: - READ_ONLY_API=${READ_ONLY_API:-no} # Keep IBC aligned with the upstream image defaults so paper/live # sessions recover after missed 2FA or stale-session prompts. + - TWOFA_DEVICE=${TWOFA_DEVICE:-} - TWOFA_TIMEOUT_ACTION=${TWOFA_TIMEOUT_ACTION:-restart} - RELOGIN_AFTER_TWOFA_TIMEOUT=${RELOGIN_AFTER_TWOFA_TIMEOUT:-yes} - EXISTING_SESSION_DETECTED_ACTION=${EXISTING_SESSION_DETECTED_ACTION:-primary} + # Set to no when IBC should use IBKR Mobile push approval instead of + # the local TOTP auto-fill helper. + - IBKR_2FA_AUTOFILL=${IBKR_2FA_AUTOFILL:-yes} # e2-micro has less than 1 GiB RAM. The upstream image defaults to # -Xmx768m, which can starve sshd/Docker/guest-agent. Keep this # configurable, but use a safer default for the current VM. diff --git a/tests/test_docker_compose_ports.sh b/tests/test_docker_compose_ports.sh index 1c2745f..9a22589 100644 --- a/tests/test_docker_compose_ports.sh +++ b/tests/test_docker_compose_ports.sh @@ -31,9 +31,11 @@ grep -Fq ' - "4001:4003"' "$compose_file" grep -Fq ' - "4002:4004"' "$compose_file" grep -Fq ' - TWS_ACCEPT_INCOMING=${TWS_ACCEPT_INCOMING:-accept}' "$compose_file" grep -Fq ' - READ_ONLY_API=${READ_ONLY_API:-no}' "$compose_file" +grep -Fq ' - TWOFA_DEVICE=${TWOFA_DEVICE:-}' "$compose_file" grep -Fq ' - TWOFA_TIMEOUT_ACTION=${TWOFA_TIMEOUT_ACTION:-restart}' "$compose_file" grep -Fq ' - RELOGIN_AFTER_TWOFA_TIMEOUT=${RELOGIN_AFTER_TWOFA_TIMEOUT:-yes}' "$compose_file" grep -Fq ' - EXISTING_SESSION_DETECTED_ACTION=${EXISTING_SESSION_DETECTED_ACTION:-primary}' "$compose_file" +grep -Fq ' - IBKR_2FA_AUTOFILL=${IBKR_2FA_AUTOFILL:-yes}' "$compose_file" grep -Fq ' - JAVA_HEAP_SIZE=${JAVA_HEAP_SIZE:-512}' "$compose_file" grep -Fq ' - IB_GATEWAY_PARALLEL_GC_THREADS=${IB_GATEWAY_PARALLEL_GC_THREADS:-2}' "$compose_file" grep -Fq ' - IB_GATEWAY_CONC_GC_THREADS=${IB_GATEWAY_CONC_GC_THREADS:-1}' "$compose_file" diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index 5870859..4913542 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -24,6 +24,8 @@ grep -Fq 'vars.IB_GATEWAY_CLOUD_RUN_EGRESS_CIDR' "$workflow_file" grep -Fq 'vars.IB_GATEWAY_ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY' "$workflow_file" grep -Fq 'vars.IB_GATEWAY_TWS_ACCEPT_INCOMING' "$workflow_file" grep -Fq 'vars.IB_GATEWAY_READ_ONLY_API' "$workflow_file" +grep -Fq 'vars.IB_GATEWAY_TWOFA_DEVICE' "$workflow_file" +grep -Fq 'vars.IB_GATEWAY_2FA_AUTOFILL' "$workflow_file" grep -Fq 'vars.IB_GATEWAY_SSH_PRIVATE_KEY_SECRET_NAME' "$workflow_file" grep -Fq 'vars.IB_GATEWAY_TWS_USERID_SECRET_NAME' "$workflow_file" grep -Fq 'vars.IB_GATEWAY_TWS_PASSWORD_SECRET_NAME' "$workflow_file" @@ -71,6 +73,8 @@ done grep -Fq '"TRADING_MODE": os.environ["IB_GATEWAY_MODE"]' "$workflow_file" grep -Fq '"ACCEPT_API_FROM_IP": os.environ["CLOUD_RUN_EGRESS_CIDR"]' "$workflow_file" +grep -Fq '"TWOFA_DEVICE": os.environ.get("TWOFA_DEVICE", "")' "$workflow_file" +grep -Fq '"IBKR_2FA_AUTOFILL": os.environ.get("IBKR_2FA_AUTOFILL", "")' "$workflow_file" grep -Fq 'REMOTE_DEPLOY_COMMAND=$(cat <