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
25 changes: 25 additions & 0 deletions 2fa_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
PRE_ENTER_DELAY = 1
XDOTOOL_TIMEOUT = 10
MIN_TOTP_SECONDS_REMAINING = 8
MAX_AUTOFILL_SUBMISSIONS_RAW = os.environ.get("IBKR_2FA_MAX_SUBMISSIONS", "1")

# Window titles to search for 2FA prompts. Live IBKR accounts can show mobile
# push / IB Key wording instead of the shorter TOTP-oriented prompts.
Expand Down Expand Up @@ -66,6 +67,13 @@
datefmt="%Y-%m-%d %H:%M:%S",
)
log = logging.getLogger("2fa_bot")
try:
MAX_AUTOFILL_SUBMISSIONS = int(MAX_AUTOFILL_SUBMISSIONS_RAW)
except ValueError:
log.error("IBKR_2FA_MAX_SUBMISSIONS must be an integer")
sys.exit(1)
Comment on lines +70 to +74
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse max-submissions only when auto-fill is active

IBKR_2FA_MAX_SUBMISSIONS is parsed at import time, so the process exits on a non-integer value before validate_config() can honor IBKR_2FA_AUTOFILL=no. That changes the previous behavior where non-autofill mode skipped TOTP-related validation and still ran in monitor-only mode; with this commit, a malformed submission-limit env var can now prevent startup even when auto-fill is intentionally disabled.

Useful? React with 👍 / 👎.

autofill_submission_count = 0
autofill_limit_warned = False


@dataclass(frozen=True)
Expand All @@ -81,6 +89,9 @@ def validate_config():
if not AUTOFILL_ENABLED:
log.info("TOTP auto-fill is disabled; bot will only log auth popup detection")
return
if MAX_AUTOFILL_SUBMISSIONS < 1:
log.error("IBKR_2FA_MAX_SUBMISSIONS must be at least 1")
sys.exit(1)
if not SECRET_KEY:
log.error("TOTP_SECRET not found in environment variables")
sys.exit(1)
Expand Down Expand Up @@ -197,6 +208,9 @@ def focus_input_area(candidate):

def submit_totp(candidate):
"""Submit a TOTP code to the selected authentication popup."""
global autofill_limit_warned
global autofill_submission_count

if not AUTOFILL_ENABLED:
log.info(
"Authentication window found (id=%s, title=%r, size=%sx%s); auto-fill disabled",
Expand All @@ -207,6 +221,16 @@ def submit_totp(candidate):
)
return

if autofill_submission_count >= MAX_AUTOFILL_SUBMISSIONS:
if not autofill_limit_warned:
log.warning(
"Authentication window found but auto-fill submission limit reached "
"(limit=%s); leaving window for manual handling",
MAX_AUTOFILL_SUBMISSIONS,
)
autofill_limit_warned = True
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",
Expand All @@ -228,6 +252,7 @@ def submit_totp(candidate):
time.sleep(PRE_ENTER_DELAY)
run_xdotool(["key", "--window", candidate.window_id, "Return"])

autofill_submission_count += 1
log.info("Auto-fill submitted, waiting for gateway response...")


Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,12 @@ VNC_SERVER_PASSWORD=your_vnc_password
TRADING_MODE=live
TWS_ACCEPT_INCOMING=accept
READ_ONLY_API=no
TWOFA_DEVICE=IB Key
TWOFA_DEVICE=Mobile Authenticator app
TWOFA_TIMEOUT_ACTION=restart
RELOGIN_AFTER_TWOFA_TIMEOUT=yes
EXISTING_SESSION_DETECTED_ACTION=primary
IBKR_2FA_AUTOFILL=no
IBKR_2FA_MAX_SUBMISSIONS=1
JAVA_HEAP_SIZE=512

# Recommended: use the exact CIDR used by your Cloud Run egress path
Expand All @@ -96,7 +97,7 @@ 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=IB Key
IB_GATEWAY_TWOFA_DEVICE=Mobile Authenticator app
IB_GATEWAY_2FA_AUTOFILL=no
```

Expand Down Expand Up @@ -211,8 +212,9 @@ 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 `IB Key` for IBKR Mobile push. |
| `IB_GATEWAY_2FA_AUTOFILL` | Optional. Set to `no` when using IBKR Mobile push instead of local TOTP auto-fill. |
| `IB_GATEWAY_TWOFA_DEVICE` | Optional. Exact IBC 2FA device name, for example `Mobile Authenticator app` for the authenticator-code prompt or `IB Key` for IBKR Mobile push. |
| `IB_GATEWAY_2FA_AUTOFILL` | Optional. Set to `no` while validating the configured TOTP secret or when using IBKR Mobile push instead of local TOTP auto-fill. |
| `IBKR_2FA_MAX_SUBMISSIONS` | Optional container env. Defaults to `1` so an invalid TOTP secret does not keep submitting codes. |

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,
Expand Down Expand Up @@ -366,11 +368,12 @@ VNC_SERVER_PASSWORD=your_vnc_password
TRADING_MODE=live
TWS_ACCEPT_INCOMING=accept
READ_ONLY_API=no
TWOFA_DEVICE=IB Key
TWOFA_DEVICE=Mobile Authenticator app
TWOFA_TIMEOUT_ACTION=restart
RELOGIN_AFTER_TWOFA_TIMEOUT=yes
EXISTING_SESSION_DETECTED_ACTION=primary
IBKR_2FA_AUTOFILL=no
IBKR_2FA_MAX_SUBMISSIONS=1
JAVA_HEAP_SIZE=512

ACCEPT_API_FROM_IP=10.8.0.0/26
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ services:
# 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}
- IBKR_2FA_MAX_SUBMISSIONS=${IBKR_2FA_MAX_SUBMISSIONS:-1}
# 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.
Expand Down
1 change: 1 addition & 0 deletions tests/test_docker_compose_ports.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ grep -Fq ' - TWOFA_TIMEOUT_ACTION=${TWOFA_TIMEOUT_ACTION:-restart}' "$compo
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 ' - IBKR_2FA_MAX_SUBMISSIONS=${IBKR_2FA_MAX_SUBMISSIONS:-1}' "$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"
Expand Down