Skip to content

dudaanton/door-knocker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Door Knocker

A Rust service for temporarily granting access through UFW via HTTP requests with JWT authentication.

Typical use case

NGINX from a private network is exposed through a proxy (e.g., frp) to a publicly accessible server. UFW is configured on this server to block unknown traffic from unfamiliar IPs.

Typically, UFW contains a list of known IPs, such as VPN/Proxy/corporate networks, etc. However, there are cases when VPN is unavailable (e.g., blocked by the ISP).

In such cases, you need to quickly temporarily allow access from your current IP address and then remove it again. But this is time-consuming and sometimes impossible (when accessing the internet from a phone, or when you need to grant access to someone else — then you have to ask them for their current IP).

This project was created specifically for these purposes. With it, you can quickly grant access to your current IP, even while using mobile internet — for example, by setting up an iOS shortcut that creates a token and authenticates on the server with a single tap.

How It Works

  1. The client generates a JWT token specifying their IP and signs it with a secret key
  2. The client sends a POST request to /knock with the token
  3. The service validates the token and adds a UFW rule allow from <IP>
  4. After the specified time (TTL), the rule is automatically removed

Features

  • JWT authentication with replay attack protection (JTI)
  • Automatic removal of rules after TTL expiration
  • Rate limiting — 10 requests per minute per IP
  • CORS — configurable origins
  • Validation — strict verification of IP addresses and key names

Installation

# Build
cargo build --release

# Copy binary
sudo cp target/release/door-knocker /usr/local/bin/

# Create config directory
sudo mkdir -p /etc/door-knocker
sudo cp config.example.toml /etc/door-knocker/config.toml
sudo chmod 600 /etc/door-knocker/config.toml

# Install systemd service
sudo cp door-knocker.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now door-knocker

Configuration

[server]
bind = "0.0.0.0:8080"
# Optional: restrict CORS origins
# If not specified or empty list — all origins are allowed
cors_origins = [
    "https://example.com",
    "http://localhost:3000"
]

[ufw]
rule_ttl_seconds = 3600  # Rule lifetime (1 hour)

[tokens]
# Format: key_name = "secret"
# The name is used in the UFW rule comment
alice = "super-secret-token-alice"
bob = "another-secret-token-bob"

Key name requirements:

  • Only a-z, A-Z, 0-9, -, _
  • Maximum 64 characters

API

POST /knock

Opens temporary access for an IP address.

Request:

{
  "token": "<JWT>"
}

JWT payload:

{
  "ip": "203.0.113.42",
  "key_name": "alice",
  "iat": 1732982400,
  "exp": 1732982460,
  "jti": "550e8400-e29b-41d4-a716-446655440000"
}
Field Description
ip Client IP address (IPv4 or IPv6)
key_name Key name from config
iat Issued at time (Unix timestamp)
exp Token expiration time
jti Unique token ID (UUID v4)

Response (200 OK):

{
  "status": "ok",
  "ip": "203.0.113.42",
  "expires_at": "2024-11-30T15:00:00+00:00",
  "ttl_seconds": 3600
}

Errors:

  • 401 Unauthorized — invalid token, unknown key, or token reuse
  • 400 Bad Request — invalid IP in token
  • 429 Too Many Requests — rate limit exceeded
  • 500 Internal Server Error — UFW error

Client Examples

Python

import jwt
import requests
import uuid
import time

SECRET = "super-secret-token-alice"
KEY_NAME = "alice"
SERVER = "http://your-server.com:8080"

def knock(my_ip: str):
    now = int(time.time())
    payload = {
        "ip": my_ip,
        "key_name": KEY_NAME,
        "iat": now,
        "exp": now + 60,  # Token lives for 60 seconds
        "jti": str(uuid.uuid4()),
    }
    token = jwt.encode(payload, SECRET, algorithm="HS256")
    
    response = requests.post(
        f"{SERVER}/knock",
        json={"token": token},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

# Usage
result = knock("203.0.113.42")
print(f"Access granted until {result['expires_at']}")

curl + jwt-cli

# Requires jwt-cli: cargo install jwt-cli
TOKEN=$(jwt encode \
  --secret "super-secret-token-alice" \
  --alg HS256 \
  '{"ip":"203.0.113.42","key_name":"alice","iat":'$(date +%s)',"exp":'$(($(date +%s)+60))',"jti":"'$(uuidgen)'"}')

curl -X POST http://your-server.com:8080/knock \
  -H "Content-Type: application/json" \
  -d "{\"token\":\"$TOKEN\"}"

Security

Attack Protection

Threat Protection
Replay attacks Unique jti in each token, storage of used JTIs
Brute force Rate limiting (10 req/min per IP)
Clock skew ±30 second tolerance when validating timestamps
Command injection Strict validation of IP and username before passing to UFW
Token interception IP embedded in token + one-time JTI — interception is useless

Recommendations

  1. Short token TTL — 30-60 seconds is sufficient
  2. Unique secrets — different keys for different users
  3. Monitoring — logs are written via tracing, level is configured via RUST_LOG

About HTTPS: the scheme with a signed one-time token and fixed IP makes interception useless. HTTPS is optional and only needed to hide the fact of the request or the IP in the token.

UFW Rules

The service creates rules like:

ufw allow from 203.0.113.42 comment "door-knocker:alice:1732982400"

The comment contains the key name and creation timestamp for auditing.

Logging

# Levels: error, warn, info, debug, trace
RUST_LOG=info door-knocker --config /etc/door-knocker/config.toml

# Only UFW module in debug
RUST_LOG=door_knocker::ufw=debug door-knocker

License

GPL-3.0

About

A Rust service for temporarily granting access through UFW via HTTP requests with JWT authentication

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors