A Rust service for temporarily granting access through UFW via HTTP requests with JWT authentication.
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.
- The client generates a JWT token specifying their IP and signs it with a secret key
- The client sends a POST request to
/knockwith the token - The service validates the token and adds a UFW rule
allow from <IP> - After the specified time (TTL), the rule is automatically removed
- 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
# 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[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
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 reuse400 Bad Request— invalid IP in token429 Too Many Requests— rate limit exceeded500 Internal Server Error— UFW error
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']}")# 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\"}"| 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 |
- Short token TTL — 30-60 seconds is sufficient
- Unique secrets — different keys for different users
- Monitoring — logs are written via
tracing, level is configured viaRUST_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.
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.
# 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-knockerGPL-3.0