Commit c3a18070 authored by nanahira's avatar nanahira

first

parents
__pycache__
*.pyc
*.pyo
venv
*.http
.venv
.git*
Dockerfile
.dockerignore
/config.yaml
.idea
.vscode
\ No newline at end of file
__pycache__
*.pyc
*.pyo
venv
*.http
/config.yaml
.idea
.vscode
\ No newline at end of file
stages:
- build
- deploy
variables:
GIT_DEPTH: "1"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
.build-image:
stage: build
script:
- docker build --pull -t $TARGET_IMAGE .
- docker push $TARGET_IMAGE
build-x86:
extends: .build-image
tags:
- docker
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86
build-arm:
extends: .build-image
tags:
- docker-arm
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
.deploy:
stage: deploy
tags:
- docker
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
- docker manifest create $TARGET_IMAGE --amend $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86 --amend
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
- docker manifest push $TARGET_IMAGE
deploy_latest:
extends: .deploy
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:latest
only:
- master
deploy_branch:
extends: .deploy
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N802" />
</list>
</option>
</inspection_tool>
</profile>
</component>
\ No newline at end of file
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ygopro-proxy.iml" filepath="$PROJECT_DIR$/.idea/ygopro-proxy.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (ygopro-proxy)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
FROM python:3.10
WORKDIR /app
COPY ./requirements.txt ./
RUN pip install --no-cache -r ./requirements.txt
COPY . ./
COPY ./config.example.yaml ./config.yaml
ENTRYPOINT ["python"]
CMD ["main.py"]
# ygopro-proxy
A reverse proxy for YGOPro, just like Nginx or BungeeCord, but for YGOPro servers.
## How to use
1. Copy `config.example.yaml` to `config.yaml`, and edit it to your needs.
2. `pip install -r requirements.txt`
3. Run `python main.py`
## Docker image
Mount config file to `/app/config.yaml` and run the container.
```bash
docker run -d -p 7911:7911 -v /path/to/config.yaml:/app/config.yaml git-registry.moenext.com/mycard/ygopro-proxy
```
\ No newline at end of file
host: 0.0.0.0
port: 7911
trusted_proxies:
- 127.0.0.0/8
- ::1
routes:
- match: ygo.example.com
to: 10.0.0.2:7911
- match: '*.example.com'
to: 10.0.0.3:7911
import asyncio
import struct
import yaml
import ipaddress
import fnmatch
import logging
CTOS_EXTERNAL_ADDRESS = 0x17
# Setup logging to stdout
logger = logging.getLogger("proxy")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
# Load YAML config
with open("config.yaml") as f:
CONFIG = yaml.safe_load(f)
trusted_proxies = [ipaddress.ip_network(net) for net in CONFIG.get("trusted_proxies", [])]
def is_trusted(ip):
addr = ipaddress.ip_address(ip)
return any(addr in net for net in trusted_proxies)
def match_route(hostname: str):
for route in CONFIG["routes"]:
if fnmatch.fnmatch(hostname, route["match"]):
to = route["to"]
if ":" in to:
host, port = to.rsplit(":", 1)
return host, int(port)
return None, None
def parse_utf16_hostname(data: bytes):
hostname = ""
for i in range(0, len(data), 2):
wchar = struct.unpack("<H", data[i:i+2])[0]
if wchar == 0:
break
hostname += chr(wchar)
return hostname
async def send_chat(writer, msg: str, player_type: int):
# Truncate and encode msg to UTF-16-LE, with null terminator
encoded = msg.encode('utf-16le')[:510] # max 255 UTF-16 chars
encoded += struct.pack('<H', 0) # null terminator
payload = struct.pack('<H', player_type) + encoded
packet = struct.pack('<H', len(payload) + 1) + bytes([0x19]) + payload
writer.write(packet)
await writer.drain()
async def close_connection(writer):
try:
payload = struct.pack('<BBBBI', 1, 0, 0, 0, 9) # msg=1, code=9
packet = struct.pack('<H', len(payload) + 1) + bytes([0x02]) + payload # STOC_ERROR_MSG = 0x02
writer.write(packet)
await writer.drain()
except Exception as e:
logger.warning(f"Failed to send error message before closing: {e}")
writer.close()
await writer.wait_closed()
async def handle_client(reader, writer):
peer_ip = writer.get_extra_info("peername")[0]
is_proxy = is_trusted(peer_ip)
try:
try:
# Read packet header
header = await asyncio.wait_for(reader.readexactly(2), timeout=5.0)
length = struct.unpack("<H", header)[0]
packet_name = await asyncio.wait_for(reader.readexactly(1), timeout=5.0)
packet_id = packet_name[0]
if packet_id != CTOS_EXTERNAL_ADDRESS:
logger.warning(f"First packet is not CTOS_EXTERNAL_ADDRESS from {peer_ip}, closing.")
await send_chat(writer, "400 Bad Request: CTOS_EXTERNAL_ADDRESS not found", player_type=11)
await close_connection(writer)
return
if length < 6 or length > 516:
logger.warning(f"Invalid packet length {length} from {peer_ip}, closing.")
await close_connection(writer)
return
payload = await asyncio.wait_for(reader.readexactly(length - 1), timeout=5.0)
except asyncio.TimeoutError:
logger.warning(f"Timeout while waiting for payload from {peer_ip}, closing.")
await close_connection(writer)
return
real_ip = ipaddress.IPv4Address(payload[0:4])
real_ip_str = str(real_ip)
real_ip_int = int(real_ip)
hostname = parse_utf16_hostname(payload[4:])
if is_proxy and real_ip_int != 0:
client_ip = real_ip_str
else:
if not is_proxy and real_ip_int != 0:
logger.warning(f"Untrusted IP {peer_ip} tried to spoof real_ip={real_ip_str}")
client_ip = peer_ip
target_host, target_port = match_route(hostname)
if not target_host:
logger.warning(f"No route found for hostname: {hostname} from {client_ip}")
await send_chat(writer, f"404 Not Found: Host [{hostname}] not found", player_type=11)
await close_connection(writer)
return
logger.info(f"{client_ip} requested {hostname} → forwarding to {target_host}:{target_port}")
# Connect to target server
try:
remote_reader, remote_writer = await asyncio.wait_for(
asyncio.open_connection(target_host, target_port), timeout=5.0)
except Exception as e:
logger.warning(f"Failed to connect to {target_host}:{target_port} for client {client_ip}: {e}")
await send_chat(writer, f"502 Bad Gateway: Host [{hostname}] cannot be connected", player_type=11)
await close_connection(writer)
return
# Overwrite real_ip in payload with resolved client_ip
try:
payload = ipaddress.IPv4Address(client_ip).packed + payload[4:]
except Exception as e:
logger.warning(f"Failed to write real_ip for {client_ip}: {e}")
# Forward first packet
remote_writer.write(header + packet_name + payload)
await remote_writer.drain()
async def pipe(src, dst):
try:
while not src.at_eof():
data = await src.read(4096)
if not data:
break
dst.write(data)
await dst.drain()
except Exception:
pass
finally:
dst.close()
await asyncio.gather(
pipe(reader, remote_writer),
pipe(remote_reader, writer)
)
except Exception as e:
logger.error(f"Error handling client {peer_ip}: {e}")
finally:
writer.close()
async def main():
server = await asyncio.start_server(handle_client, CONFIG["host"], CONFIG["port"])
addr = server.sockets[0].getsockname()
logger.info(f"Proxy listening on {addr}")
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment