Xray works

This commit is contained in:
AB from home.homenet
2025-08-08 05:46:36 +03:00
parent 56b0b160e3
commit 787432cbcf
46 changed files with 5625 additions and 3551 deletions

View File

@@ -0,0 +1,55 @@
"""Xray API models"""
from .base import (
BaseXrayModel, XrayConfig, XrayProtocol,
TransportProtocol, SecurityType
)
from .protocols import (
# VLESS
VLESSAccount, VLESSClient, VLESSInboundConfig,
# VMess
VMeSSAccount, VMeSSUser, VMeSSInboundConfig, VMeSSSecurityConfig,
# Trojan
TrojanAccount, TrojanUser, TrojanServerConfig, TrojanFallback,
# Shadowsocks
ShadowsocksAccount, ShadowsocksUser, ShadowsocksServerConfig,
# Utils
generate_uuid, validate_uuid, create_protocol_config
)
from .transports import (
StreamSettings, TCPSettings, WebSocketSettings, GRPCSettings,
HTTPSettings, XHTTPSettings, KCPSettings, QUICSettings, DomainSocketSettings,
create_tcp_stream, create_ws_stream, create_grpc_stream, create_http_stream, create_xhttp_stream
)
from .security import (
TLSConfig, REALITYConfig, XTLSConfig, Certificate,
create_tls_config, create_reality_config, create_reality_client_config,
generate_self_signed_certificate, create_tls_config_with_self_signed
)
from .inbound import (
InboundConfig, ReceiverConfig, SniffingConfig, InboundBuilder
)
__all__ = [
# Base
'BaseXrayModel', 'XrayConfig', 'XrayProtocol', 'TransportProtocol', 'SecurityType',
# Protocols
'VLESSAccount', 'VLESSClient', 'VLESSInboundConfig',
'VMeSSAccount', 'VMeSSUser', 'VMeSSInboundConfig', 'VMeSSSecurityConfig',
'TrojanAccount', 'TrojanUser', 'TrojanServerConfig', 'TrojanFallback',
'ShadowsocksAccount', 'ShadowsocksUser', 'ShadowsocksServerConfig',
'generate_uuid', 'validate_uuid', 'create_protocol_config',
# Transports
'StreamSettings', 'TCPSettings', 'WebSocketSettings', 'GRPCSettings',
'HTTPSettings', 'XHTTPSettings', 'KCPSettings', 'QUICSettings', 'DomainSocketSettings',
'create_tcp_stream', 'create_ws_stream', 'create_grpc_stream', 'create_http_stream', 'create_xhttp_stream',
# Security
'TLSConfig', 'REALITYConfig', 'XTLSConfig', 'Certificate',
'create_tls_config', 'create_reality_config', 'create_reality_client_config',
'generate_self_signed_certificate', 'create_tls_config_with_self_signed',
# Inbound
'InboundConfig', 'ReceiverConfig', 'SniffingConfig', 'InboundBuilder',
]

View File

@@ -0,0 +1,97 @@
"""Base models for xray-api library"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, asdict, field
from typing import Dict, Any, Optional, Type, TypeVar
import json
from enum import Enum
T = TypeVar('T', bound='BaseXrayModel')
class BaseXrayModel(ABC):
"""Base class for all Xray configuration models"""
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary for storage"""
if hasattr(self, '__dataclass_fields__'):
return self._clean_dict(asdict(self))
return self._clean_dict(self.__dict__.copy())
def to_xray_json(self) -> Dict[str, Any]:
"""Convert model to Xray API format"""
return self.to_dict()
@classmethod
def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
"""Create model instance from dictionary"""
return cls(**data)
def _clean_dict(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Remove None values and empty collections"""
cleaned = {}
for key, value in data.items():
if value is None:
continue
if isinstance(value, (list, dict)) and not value:
continue
if isinstance(value, BaseXrayModel):
cleaned[key] = value.to_dict()
elif isinstance(value, list):
cleaned[key] = [
item.to_dict() if isinstance(item, BaseXrayModel) else item
for item in value
]
elif isinstance(value, Enum):
cleaned[key] = value.value
else:
cleaned[key] = value
return cleaned
def to_json(self) -> str:
"""Convert to JSON string"""
return json.dumps(self.to_dict(), indent=2)
@dataclass
class XrayConfig(BaseXrayModel):
"""Base configuration class"""
_TypedMessage_: Optional[str] = field(default=None, init=False)
def __post_init__(self):
"""Set TypedMessage after initialization"""
if self._TypedMessage_ is None and hasattr(self, '__xray_type__'):
self._TypedMessage_ = self.__xray_type__
class XrayProtocol(str, Enum):
"""Supported Xray protocols"""
VLESS = "vless"
VMESS = "vmess"
TROJAN = "trojan"
SHADOWSOCKS = "shadowsocks"
DOKODEMO = "dokodemo-door"
FREEDOM = "freedom"
BLACKHOLE = "blackhole"
DNS = "dns"
HTTP = "http"
SOCKS = "socks"
class TransportProtocol(str, Enum):
"""Transport protocols"""
TCP = "tcp"
KCP = "kcp"
WS = "ws"
HTTP = "http"
XHTTP = "xhttp"
DOMAINSOCKET = "domainsocket"
QUIC = "quic"
GRPC = "grpc"
class SecurityType(str, Enum):
"""Security types"""
NONE = "none"
TLS = "tls"
REALITY = "reality"
XTLS = "xtls"

View File

@@ -0,0 +1,176 @@
"""Inbound configuration models"""
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List, Union
from .base import BaseXrayModel, XrayConfig, XrayProtocol
from .protocols import (
VLESSInboundConfig, VMeSSInboundConfig,
TrojanServerConfig, ShadowsocksServerConfig
)
from .transports import StreamSettings
@dataclass
class SniffingConfig(BaseXrayModel):
"""Traffic sniffing configuration"""
enabled: bool = True
destOverride: Optional[List[str]] = None
metadataOnly: bool = False
def __post_init__(self):
if self.destOverride is None:
self.destOverride = ["http", "tls"]
@dataclass
class ReceiverConfig(XrayConfig):
"""Receiver configuration for inbound"""
__xray_type__ = "xray.app.proxyman.ReceiverConfig"
listen: str = "0.0.0.0"
port: Optional[int] = None
portList: Optional[Union[int, str]] = None # Can be int or range like "10000-20000"
streamSettings: Optional[StreamSettings] = None
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray format"""
config = {"listen": self.listen}
# Either port or portList must be set
if self.port is not None:
config["port"] = self.port
elif self.portList is not None:
config["portList"] = self.portList
else:
raise ValueError("Either port or portList must be specified")
if self.streamSettings:
config["streamSettings"] = self.streamSettings.to_xray_json()
return config
@dataclass
class InboundConfig(BaseXrayModel):
"""Complete inbound configuration"""
tag: str
protocol: XrayProtocol
settings: Union[VLESSInboundConfig, VMeSSInboundConfig, TrojanServerConfig, ShadowsocksServerConfig]
listen: str = "0.0.0.0"
port: Optional[int] = None
portList: Optional[Union[int, str]] = None
streamSettings: Optional[StreamSettings] = None
sniffing: Optional[SniffingConfig] = None
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray API format with proper field order and structure"""
config = {
"listen": self.listen,
"tag": self.tag,
"protocol": self.protocol.value if hasattr(self.protocol, 'value') else str(self.protocol),
}
# Add port or portList (port comes before protocol in working format)
if self.port is not None:
config["port"] = self.port
elif self.portList is not None:
config["portList"] = self.portList
else:
raise ValueError("Either port or portList must be specified")
# Add protocol settings with _TypedMessage_
settings = self.settings.to_xray_json()
if "_TypedMessage_" not in settings:
# Add _TypedMessage_ based on protocol
protocol_type_map = {
"vless": "xray.proxy.vless.inbound.Config",
"vmess": "xray.proxy.vmess.inbound.Config",
"trojan": "xray.proxy.trojan.inbound.Config",
"shadowsocks": "xray.proxy.shadowsocks.inbound.Config"
}
protocol_name = self.protocol.value if hasattr(self.protocol, 'value') else str(self.protocol)
if protocol_name in protocol_type_map:
settings["_TypedMessage_"] = protocol_type_map[protocol_name]
config["settings"] = settings
# Add stream settings
if self.streamSettings:
config["streamSettings"] = self.streamSettings.to_xray_json()
else:
config["streamSettings"] = {"network": "tcp"} # Default TCP
# Add sniffing
if self.sniffing:
config["sniffing"] = self.sniffing.to_dict()
return config
# Builder for easier configuration
class InboundBuilder:
"""Builder for creating inbound configurations"""
def __init__(self, tag: str, protocol: XrayProtocol):
self.tag = tag
self.protocol = protocol
self._settings = None
self._listen = "0.0.0.0"
self._port = None
self._port_list = None
self._stream_settings = None
self._sniffing = None
def listen(self, address: str) -> 'InboundBuilder':
"""Set listen address"""
self._listen = address
return self
def port(self, port: int) -> 'InboundBuilder':
"""Set single port"""
self._port = port
self._port_list = None
return self
def port_range(self, start: int, end: int) -> 'InboundBuilder':
"""Set port range"""
self._port_list = f"{start}-{end}"
self._port = None
return self
def stream_settings(self, settings: StreamSettings) -> 'InboundBuilder':
"""Set stream settings"""
self._stream_settings = settings
return self
def sniffing(self, enabled: bool = True, dest_override: Optional[List[str]] = None) -> 'InboundBuilder':
"""Configure sniffing"""
self._sniffing = SniffingConfig(
enabled=enabled,
destOverride=dest_override
)
return self
def protocol_settings(self, settings: Union[VLESSInboundConfig, VMeSSInboundConfig, TrojanServerConfig, ShadowsocksServerConfig]) -> 'InboundBuilder':
"""Set protocol-specific settings"""
self._settings = settings
return self
def build(self) -> InboundConfig:
"""Build the inbound configuration"""
if not self._settings:
raise ValueError("Protocol settings must be specified")
if not self._port and not self._port_list:
raise ValueError("Either port or port range must be specified")
return InboundConfig(
tag=self.tag,
protocol=self.protocol,
settings=self._settings,
listen=self._listen,
port=self._port,
portList=self._port_list,
streamSettings=self._stream_settings,
sniffing=self._sniffing
)

View File

@@ -0,0 +1,266 @@
"""Protocol models for Xray"""
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any
from uuid import uuid4
import re
from .base import BaseXrayModel, XrayConfig, XrayProtocol
def validate_uuid(uuid_str: str) -> bool:
"""Validate UUID format"""
pattern = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.I)
return bool(pattern.match(uuid_str))
def generate_uuid() -> str:
"""Generate new UUID"""
return str(uuid4())
# VLESS Protocol
@dataclass
class VLESSAccount(XrayConfig):
"""VLESS account configuration"""
__xray_type__ = "xray.proxy.vless.Account"
id: str
flow: Optional[str] = None
encryption: str = "none"
def __post_init__(self):
super().__post_init__()
if not validate_uuid(self.id):
raise ValueError(f"Invalid UUID: {self.id}")
@dataclass
class VLESSClient(BaseXrayModel):
"""VLESS client configuration"""
email: str
account: VLESSAccount
level: int = 0
@classmethod
def create(cls, email: str, uuid: Optional[str] = None, flow: Optional[str] = None) -> 'VLESSClient':
"""Create VLESS client with optional UUID generation"""
if uuid is None:
uuid = generate_uuid()
account = VLESSAccount(id=uuid, flow=flow)
return cls(email=email, account=account)
@dataclass
class VLESSInboundConfig(XrayConfig):
"""VLESS inbound configuration"""
__xray_type__ = "xray.proxy.vless.inbound.Config"
clients: List[VLESSClient] = field(default_factory=list)
decryption: str = "none"
fallbacks: Optional[List[Dict[str, Any]]] = None
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray API format with proper structure"""
config = {
"_TypedMessage_": self.__xray_type__,
"clients": [],
"decryption": self.decryption
}
# Convert clients to proper format
for client in self.clients:
client_data = {
"id": client.account.id,
"level": client.level,
"email": client.email
}
if client.account.flow:
client_data["flow"] = client.account.flow
config["clients"].append(client_data)
if self.fallbacks:
config["fallbacks"] = self.fallbacks
return config
# VMess Protocol
@dataclass
class VMeSSSecurityConfig(BaseXrayModel):
"""VMess security configuration"""
type: str = "AUTO" # AUTO, AES-128-GCM, CHACHA20-POLY1305, NONE
@dataclass
class VMeSSAccount(XrayConfig):
"""VMess account configuration"""
__xray_type__ = "xray.proxy.vmess.Account"
id: str
securitySettings: Optional[VMeSSSecurityConfig] = None
def __post_init__(self):
super().__post_init__()
if not validate_uuid(self.id):
raise ValueError(f"Invalid UUID: {self.id}")
if self.securitySettings is None:
self.securitySettings = VMeSSSecurityConfig()
@dataclass
class VMeSSUser(BaseXrayModel):
"""VMess user configuration"""
email: str
account: VMeSSAccount
level: int = 0
@classmethod
def create(cls, email: str, uuid: Optional[str] = None, security: str = "AUTO") -> 'VMeSSUser':
"""Create VMess user with optional UUID generation"""
if uuid is None:
uuid = generate_uuid()
account = VMeSSAccount(
id=uuid,
securitySettings=VMeSSSecurityConfig(type=security)
)
return cls(email=email, account=account)
@dataclass
class VMeSSInboundConfig(XrayConfig):
"""VMess inbound configuration"""
__xray_type__ = "xray.proxy.vmess.inbound.Config"
user: List[VMeSSUser] = field(default_factory=list)
disableInsecureEncryption: bool = False
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray API format with proper structure"""
config = {
"_TypedMessage_": self.__xray_type__,
"clients": []
}
# Convert users to proper format
for user in self.user:
client_data = {
"id": user.account.id,
"level": user.level,
"email": user.email,
"alterId": 0 # VMess specific
}
config["clients"].append(client_data)
return config
# Trojan Protocol
@dataclass
class TrojanAccount(XrayConfig):
"""Trojan account configuration"""
__xray_type__ = "xray.proxy.trojan.Account"
password: str
@classmethod
def generate_password(cls) -> str:
"""Generate secure password"""
return generate_uuid()
@dataclass
class TrojanUser(BaseXrayModel):
"""Trojan user configuration"""
email: str
account: TrojanAccount
level: int = 0
@classmethod
def create(cls, email: str, password: Optional[str] = None) -> 'TrojanUser':
"""Create Trojan user with optional password generation"""
if password is None:
password = TrojanAccount.generate_password()
account = TrojanAccount(password=password)
return cls(email=email, account=account)
@dataclass
class TrojanFallback(BaseXrayModel):
"""Trojan fallback configuration"""
dest: str
type: str = "tcp"
xver: int = 0
@dataclass
class TrojanServerConfig(XrayConfig):
"""Trojan server configuration"""
__xray_type__ = "xray.proxy.trojan.ServerConfig"
users: List[TrojanUser] = field(default_factory=list)
fallbacks: Optional[List[TrojanFallback]] = None
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray API format with proper structure"""
config = {
"_TypedMessage_": self.__xray_type__,
"clients": []
}
# Convert users to proper format
for user in self.users:
client_data = {
"password": user.account.password,
"level": user.level,
"email": user.email
}
config["clients"].append(client_data)
if self.fallbacks:
config["fallbacks"] = [fb.to_dict() for fb in self.fallbacks]
return config
# Shadowsocks Protocol
@dataclass
class ShadowsocksAccount(XrayConfig):
"""Shadowsocks account configuration"""
__xray_type__ = "xray.proxy.shadowsocks.Account"
method: str # aes-256-gcm, aes-128-gcm, chacha20-poly1305, etc.
password: str
@dataclass
class ShadowsocksUser(BaseXrayModel):
"""Shadowsocks user configuration"""
email: str
account: ShadowsocksAccount
level: int = 0
@dataclass
class ShadowsocksServerConfig(XrayConfig):
"""Shadowsocks server configuration"""
__xray_type__ = "xray.proxy.shadowsocks.ServerConfig"
users: List[ShadowsocksUser] = field(default_factory=list)
network: str = "tcp,udp"
# Protocol config factory
def create_protocol_config(protocol: XrayProtocol, **kwargs) -> XrayConfig:
"""Factory to create protocol configurations"""
protocol_map = {
XrayProtocol.VLESS: VLESSInboundConfig,
XrayProtocol.VMESS: VMeSSInboundConfig,
XrayProtocol.TROJAN: TrojanServerConfig,
XrayProtocol.SHADOWSOCKS: ShadowsocksServerConfig,
}
config_class = protocol_map.get(protocol)
if not config_class:
raise ValueError(f"Unsupported protocol: {protocol}")
return config_class(**kwargs)

View File

@@ -0,0 +1,389 @@
"""Security configuration models for Xray"""
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any, Tuple
from pathlib import Path
import secrets
from datetime import datetime, timedelta
from .base import BaseXrayModel, XrayConfig, SecurityType
# TLS Configuration
@dataclass
class Certificate(BaseXrayModel):
"""TLS certificate configuration"""
certificateFile: Optional[str] = None
keyFile: Optional[str] = None
certificate: Optional[List[str]] = None # PEM format lines
key: Optional[List[str]] = None # PEM format lines
usage: str = "encipherment"
ocspStapling: int = 3600
oneTimeLoading: bool = False
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray format"""
config = {}
if self.certificateFile and self.keyFile:
config["certificateFile"] = self.certificateFile
config["keyFile"] = self.keyFile
elif self.certificate and self.key:
config["certificate"] = self.certificate
config["key"] = self.key
config["usage"] = self.usage
if self.ocspStapling:
config["ocspStapling"] = self.ocspStapling
if self.oneTimeLoading:
config["OneTimeLoading"] = self.oneTimeLoading
return config
@dataclass
class TLSConfig(XrayConfig):
"""TLS configuration"""
__xray_type__ = "xray.transport.internet.tls.Config"
serverName: Optional[str] = None
allowInsecure: bool = False
alpn: Optional[List[str]] = None
minVersion: str = "1.2"
maxVersion: str = "1.3"
preferServerCipherSuites: bool = True
cipherSuites: Optional[str] = None
certificates: Optional[List[Certificate]] = None
disableSystemRoot: bool = False
enableSessionResumption: bool = False
fingerprint: Optional[str] = None # Client-side
pinnedPeerCertificateChainSha256: Optional[List[str]] = None
rejectUnknownSni: bool = False # Server-side
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray format with proper field handling"""
config = super().to_xray_json()
# Handle certificates properly
if self.certificates:
config["certificates"] = [cert.to_xray_json() for cert in self.certificates]
return config
# REALITY Configuration
@dataclass
class REALITYConfig(XrayConfig):
"""REALITY configuration"""
__xray_type__ = "xray.transport.internet.reality.Config"
# Server-side
show: bool = False
dest: Optional[str] = None # e.g., "example.com:443"
xver: int = 0
serverNames: Optional[List[str]] = None
privateKey: Optional[str] = None
shortIds: Optional[List[str]] = None
# Client-side
serverName: Optional[str] = None
fingerprint: str = "chrome"
publicKey: Optional[str] = None
shortId: Optional[str] = None
spiderX: Optional[str] = None
@classmethod
def generate_keys(cls) -> Dict[str, str]:
"""Generate REALITY key pair using xray x25519"""
import subprocess
try:
# Use xray x25519 to generate proper keys
result = subprocess.run(['xray', 'x25519'], capture_output=True, text=True, check=True)
lines = result.stdout.strip().split('\n')
private_key = ""
public_key = ""
for line in lines:
if line.startswith('Private key:'):
private_key = line.split(': ')[1].strip()
elif line.startswith('Public key:'):
public_key = line.split(': ')[1].strip()
return {
"privateKey": private_key,
"publicKey": public_key
}
except (subprocess.CalledProcessError, FileNotFoundError, IndexError):
# Fallback to base64 encoded random bytes (32 bytes for X25519)
import base64
private_bytes = secrets.token_bytes(32)
public_bytes = secrets.token_bytes(32)
return {
"privateKey": base64.b64encode(private_bytes).decode().rstrip('='),
"publicKey": base64.b64encode(public_bytes).decode().rstrip('=')
}
@classmethod
def generate_short_id(cls) -> str:
"""Generate random short ID (1-16 hex chars)"""
# Generate 1-8 bytes (2-16 hex chars)
length = secrets.randbelow(8) + 1
return secrets.token_hex(length)
# XTLS Configuration (deprecated but still supported)
@dataclass
class XTLSConfig(XrayConfig):
"""XTLS configuration (legacy)"""
__xray_type__ = "xray.transport.internet.xtls.Config"
serverName: Optional[str] = None
allowInsecure: bool = False
alpn: Optional[List[str]] = None
minVersion: str = "1.2"
maxVersion: str = "1.3"
preferServerCipherSuites: bool = True
cipherSuites: Optional[str] = None
certificates: Optional[List[Certificate]] = None
disableSystemRoot: bool = False
fingerprint: Optional[str] = None
rejectUnknownSni: bool = False
# Security factory
def create_tls_config(
server_name: Optional[str] = None,
cert_file: Optional[str] = None,
key_file: Optional[str] = None,
alpn: Optional[List[str]] = None,
fingerprint: Optional[str] = None,
**kwargs
) -> TLSConfig:
"""Create TLS configuration"""
config = TLSConfig(
serverName=server_name,
alpn=alpn or ["h2", "http/1.1"],
fingerprint=fingerprint,
**kwargs
)
if cert_file and key_file:
config.certificates = [Certificate(
certificateFile=cert_file,
keyFile=key_file
)]
return config
def create_reality_config(
dest: str,
server_names: Optional[List[str]] = None,
private_key: Optional[str] = None,
short_ids: Optional[List[str]] = None,
**kwargs
) -> REALITYConfig:
"""Create REALITY configuration for server"""
if not private_key:
keys = REALITYConfig.generate_keys()
private_key = keys["privateKey"]
if not short_ids:
short_ids = [REALITYConfig.generate_short_id()]
return REALITYConfig(
show=False,
dest=dest,
serverNames=server_names or [dest.split(":")[0]],
privateKey=private_key,
shortIds=short_ids,
**kwargs
)
def create_reality_client_config(
server_name: str,
public_key: str,
short_id: str,
fingerprint: str = "chrome",
**kwargs
) -> REALITYConfig:
"""Create REALITY configuration for client"""
return REALITYConfig(
serverName=server_name,
publicKey=public_key,
shortId=short_id,
fingerprint=fingerprint,
**kwargs
)
# Certificate generation utilities
def generate_self_signed_certificate(
common_name: str = "localhost",
san_list: Optional[List[str]] = None,
days: int = 365,
key_size: int = 2048,
save_to_files: bool = False,
cert_path: Optional[str] = None,
key_path: Optional[str] = None
) -> Tuple[str, str]:
"""
Generate self-signed certificate
Args:
common_name: Certificate common name
san_list: Subject Alternative Names (domains/IPs)
days: Certificate validity in days
key_size: RSA key size
save_to_files: Whether to save to files
cert_path: Path to save certificate
key_path: Path to save private key
Returns:
Tuple of (certificate_pem, private_key_pem)
"""
try:
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
except ImportError:
raise ImportError("cryptography package is required for certificate generation. Install with: pip install cryptography")
# Generate private key
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=key_size,
backend=default_backend()
)
# Create certificate subject
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "State"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "City"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Xray Self-Signed"),
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
])
# Create certificate builder
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(issuer)
builder = builder.public_key(private_key.public_key())
builder = builder.serial_number(x509.random_serial_number())
builder = builder.not_valid_before(datetime.utcnow())
builder = builder.not_valid_after(datetime.utcnow() + timedelta(days=days))
# Add Subject Alternative Names
san_list = san_list or [common_name]
alt_names = []
for san in san_list:
if san.replace('.', '').isdigit(): # IP address
alt_names.append(x509.IPAddress(ipaddress.ip_address(san)))
else: # Domain name
alt_names.append(x509.DNSName(san))
builder = builder.add_extension(
x509.SubjectAlternativeName(alt_names),
critical=False,
)
# Add basic constraints
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=0),
critical=True,
)
# Add key usage
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=True,
data_encipherment=False,
key_agreement=False,
key_cert_sign=True,
crl_sign=False,
encipher_only=False,
decipher_only=False,
),
critical=True,
)
# Sign certificate
certificate = builder.sign(private_key, hashes.SHA256(), default_backend())
# Serialize to PEM
cert_pem = certificate.public_bytes(serialization.Encoding.PEM).decode('utf-8')
key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
).decode('utf-8')
# Save to files if requested
if save_to_files:
cert_file = cert_path or f"{common_name}_cert.pem"
key_file = key_path or f"{common_name}_key.pem"
with open(cert_file, 'w') as f:
f.write(cert_pem)
with open(key_file, 'w') as f:
f.write(key_pem)
# Set appropriate permissions
Path(key_file).chmod(0o600)
return cert_pem, key_pem
def create_tls_config_with_self_signed(
common_name: str = "localhost",
san_list: Optional[List[str]] = None,
alpn: Optional[List[str]] = None,
**tls_kwargs
) -> Tuple[TLSConfig, str, str]:
"""
Create TLS configuration with self-signed certificate
Returns:
Tuple of (TLSConfig, certificate_pem, private_key_pem)
"""
cert_pem, key_pem = generate_self_signed_certificate(
common_name=common_name,
san_list=san_list
)
# Convert PEM to lines for Xray format
cert_lines = cert_pem.strip().split('\n')
key_lines = key_pem.strip().split('\n')
# Create certificate config
certificate = Certificate(
certificate=cert_lines,
key=key_lines,
oneTimeLoading=True
)
# Create TLS config
tls_config = TLSConfig(
serverName=common_name,
alpn=alpn or ["h2", "http/1.1"],
certificates=[certificate],
**tls_kwargs
)
return tls_config, cert_pem, key_pem
# Add missing import
try:
import ipaddress
except ImportError:
ipaddress = None

View File

@@ -0,0 +1,241 @@
"""Transport configuration models for Xray"""
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
from .base import BaseXrayModel, XrayConfig, TransportProtocol
# TCP Transport
@dataclass
class TCPSettings(XrayConfig):
"""TCP transport settings"""
__xray_type__ = "xray.transport.internet.tcp.Config"
acceptProxyProtocol: bool = False
header: Optional[Dict[str, Any]] = None
# KCP Transport
@dataclass
class KCPSettings(XrayConfig):
"""KCP transport settings"""
__xray_type__ = "xray.transport.internet.kcp.Config"
mtu: int = 1350
tti: int = 50
uplinkCapacity: int = 5
downlinkCapacity: int = 20
congestion: bool = False
readBufferSize: int = 2
writeBufferSize: int = 2
header: Optional[Dict[str, Any]] = None
# WebSocket Transport
@dataclass
class WebSocketSettings(XrayConfig):
"""WebSocket transport settings"""
__xray_type__ = "xray.transport.internet.websocket.Config"
path: str = "/"
headers: Optional[Dict[str, str]] = None
acceptProxyProtocol: bool = False
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray format"""
config = super().to_xray_json()
# Ensure headers is a dict even if empty
if self.headers:
config["headers"] = self.headers
return config
# HTTP/2 Transport
@dataclass
class HTTPSettings(XrayConfig):
"""HTTP/2 transport settings"""
__xray_type__ = "xray.transport.internet.http.Config"
path: str = "/"
host: Optional[List[str]] = None
method: str = "PUT"
headers: Optional[Dict[str, List[str]]] = None
# XHTTP Transport (New)
@dataclass
class XHTTPSettings(XrayConfig):
"""XHTTP transport settings"""
__xray_type__ = "xray.transport.internet.xhttp.Config"
path: str = "/"
host: Optional[str] = None
method: str = "GET"
headers: Optional[Dict[str, Any]] = None
mode: str = "auto"
# Domain Socket Transport
@dataclass
class DomainSocketSettings(XrayConfig):
"""Domain socket transport settings"""
__xray_type__ = "xray.transport.internet.domainsocket.Config"
path: str
abstract: bool = False
padding: bool = False
# QUIC Transport
@dataclass
class QUICSettings(XrayConfig):
"""QUIC transport settings"""
__xray_type__ = "xray.transport.internet.quic.Config"
security: str = "none"
key: str = ""
header: Optional[Dict[str, Any]] = None
# gRPC Transport
@dataclass
class GRPCSettings(XrayConfig):
"""gRPC transport settings"""
__xray_type__ = "xray.transport.internet.grpc.encoding.Config"
serviceName: str = ""
multiMode: bool = False
idle_timeout: int = 60
health_check_timeout: int = 20
permit_without_stream: bool = False
initial_windows_size: int = 0
# Stream Settings
@dataclass
class StreamSettings(BaseXrayModel):
"""Stream settings for inbound/outbound"""
network: TransportProtocol = TransportProtocol.TCP
security: Optional[str] = None
tlsSettings: Optional[Any] = None
xtlsSettings: Optional[Any] = None
realitySettings: Optional[Any] = None
tcpSettings: Optional[TCPSettings] = None
kcpSettings: Optional[KCPSettings] = None
wsSettings: Optional[WebSocketSettings] = None
httpSettings: Optional[HTTPSettings] = None
xhttpSettings: Optional[XHTTPSettings] = None
dsSettings: Optional[DomainSocketSettings] = None
quicSettings: Optional[QUICSettings] = None
grpcSettings: Optional[GRPCSettings] = None
sockopt: Optional[Dict[str, Any]] = None
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray format with correct field names"""
config = {
"network": self.network.value if isinstance(self.network, TransportProtocol) else self.network
}
if self.security:
config["security"] = self.security
# Map transport settings
transport_map = {
TransportProtocol.TCP: ("tcpSettings", self.tcpSettings),
TransportProtocol.KCP: ("kcpSettings", self.kcpSettings),
TransportProtocol.WS: ("wsSettings", self.wsSettings),
TransportProtocol.HTTP: ("httpSettings", self.httpSettings),
TransportProtocol.XHTTP: ("xhttpSettings", self.xhttpSettings),
TransportProtocol.DOMAINSOCKET: ("dsSettings", self.dsSettings),
TransportProtocol.QUIC: ("quicSettings", self.quicSettings),
TransportProtocol.GRPC: ("grpcSettings", self.grpcSettings),
}
network = self.network if isinstance(self.network, TransportProtocol) else TransportProtocol(self.network)
field_name, settings = transport_map.get(network, (None, None))
if field_name and settings:
config[field_name] = settings.to_xray_json() if hasattr(settings, 'to_xray_json') else settings
# Add security settings
if self.tlsSettings:
config["tlsSettings"] = self.tlsSettings if isinstance(self.tlsSettings, dict) else self.tlsSettings.to_xray_json()
if self.xtlsSettings:
config["xtlsSettings"] = self.xtlsSettings if isinstance(self.xtlsSettings, dict) else self.xtlsSettings.to_xray_json()
if self.realitySettings:
config["realitySettings"] = self.realitySettings if isinstance(self.realitySettings, dict) else self.realitySettings.to_xray_json()
if self.sockopt:
config["sockopt"] = self.sockopt
return config
# Factory functions
def create_tcp_stream(
security: Optional[str] = None,
**kwargs
) -> StreamSettings:
"""Create TCP stream settings"""
return StreamSettings(
network=TransportProtocol.TCP,
security=security,
tcpSettings=TCPSettings(**kwargs) if kwargs else None
)
def create_ws_stream(
path: str = "/",
headers: Optional[Dict[str, str]] = None,
security: Optional[str] = None,
**kwargs
) -> StreamSettings:
"""Create WebSocket stream settings"""
return StreamSettings(
network=TransportProtocol.WS,
security=security,
wsSettings=WebSocketSettings(path=path, headers=headers, **kwargs)
)
def create_grpc_stream(
service_name: str = "",
security: Optional[str] = None,
**kwargs
) -> StreamSettings:
"""Create gRPC stream settings"""
return StreamSettings(
network=TransportProtocol.GRPC,
security=security,
grpcSettings=GRPCSettings(serviceName=service_name, **kwargs)
)
def create_http_stream(
path: str = "/",
host: Optional[List[str]] = None,
security: Optional[str] = None,
**kwargs
) -> StreamSettings:
"""Create HTTP/2 stream settings"""
return StreamSettings(
network=TransportProtocol.HTTP,
security=security,
httpSettings=HTTPSettings(path=path, host=host, **kwargs)
)
def create_xhttp_stream(
path: str = "/",
host: Optional[str] = None,
security: Optional[str] = None,
mode: str = "auto",
**kwargs
) -> StreamSettings:
"""Create XHTTP stream settings"""
return StreamSettings(
network=TransportProtocol.XHTTP,
security=security,
xhttpSettings=XHTTPSettings(path=path, host=host, mode=mode, **kwargs)
)