mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-08-21 14:37:16 +00:00
Xray works.
This commit is contained in:
@@ -450,7 +450,7 @@ class InboundAdmin(admin.ModelAdmin):
|
||||
"""Admin for inbound template management"""
|
||||
list_display = (
|
||||
'name', 'protocol', 'port', 'network',
|
||||
'security', 'certificate_status', 'group_count'
|
||||
'security', 'group_count'
|
||||
)
|
||||
list_filter = ('protocol', 'network', 'security')
|
||||
search_fields = ('name',)
|
||||
@@ -458,10 +458,10 @@ class InboundAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
('Basic Configuration', {
|
||||
'fields': ('name', 'protocol', 'port'),
|
||||
'description': 'Domain will be taken from server client_hostname when deployed'
|
||||
'description': 'Certificates are configured per-server in Server admin → Inbound Templates tab'
|
||||
}),
|
||||
('Transport & Security', {
|
||||
'fields': ('network', 'security', 'certificate', 'listen_address')
|
||||
'fields': ('network', 'security', 'listen_address')
|
||||
}),
|
||||
('Advanced Settings', {
|
||||
'fields': ('enable_sniffing', 'full_config_display'),
|
||||
@@ -476,19 +476,6 @@ class InboundAdmin(admin.ModelAdmin):
|
||||
|
||||
readonly_fields = ('full_config_display', 'created_at', 'updated_at')
|
||||
|
||||
def certificate_status(self, obj):
|
||||
"""Display certificate status"""
|
||||
if obj.security == 'tls':
|
||||
if obj.certificate:
|
||||
if obj.certificate.is_expired:
|
||||
return format_html('<span style="color: red;">❌ Expired</span>')
|
||||
else:
|
||||
return format_html('<span style="color: green;">✅ Valid</span>')
|
||||
else:
|
||||
return format_html('<span style="color: orange;">⚠️ No cert</span>')
|
||||
return format_html('<span style="color: gray;">-</span>')
|
||||
certificate_status.short_description = 'Cert Status'
|
||||
|
||||
def group_count(self, obj):
|
||||
"""Number of groups this inbound belongs to"""
|
||||
return obj.subscriptiongroup_set.count()
|
||||
@@ -695,7 +682,7 @@ class UserSubscriptionAdmin(admin.ModelAdmin):
|
||||
# ServerInbound admin is integrated into XrayServerV2 admin, not shown in main menu
|
||||
class ServerInboundAdmin(admin.ModelAdmin):
|
||||
"""Admin for server-inbound deployment tracking"""
|
||||
list_display = ('server', 'inbound', 'active', 'deployed_at', 'updated_at')
|
||||
list_display = ('server', 'inbound', 'certificate_status', 'active', 'deployed_at', 'updated_at')
|
||||
list_filter = ('active', 'inbound__protocol', 'deployed_at')
|
||||
search_fields = ('server__name', 'inbound__name')
|
||||
date_hierarchy = 'deployed_at'
|
||||
@@ -704,13 +691,54 @@ class ServerInboundAdmin(admin.ModelAdmin):
|
||||
('Template Deployment', {
|
||||
'fields': ('server', 'inbound', 'active')
|
||||
}),
|
||||
('Certificate Configuration', {
|
||||
'fields': ('certificate', 'certificate_info'),
|
||||
'description': 'Certificate for TLS. If not specified, will be auto-selected by server hostname.'
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('deployed_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
})
|
||||
)
|
||||
|
||||
readonly_fields = ('deployed_at', 'updated_at')
|
||||
readonly_fields = ('deployed_at', 'updated_at', 'certificate_info')
|
||||
|
||||
def certificate_status(self, obj):
|
||||
"""Display certificate status"""
|
||||
if not obj.requires_certificate():
|
||||
return format_html('<span style="color: gray;">-</span>')
|
||||
|
||||
cert = obj.get_certificate()
|
||||
if cert:
|
||||
if cert.is_expired:
|
||||
return format_html('<span style="color: red;">❌ {}</span>', cert.domain)
|
||||
else:
|
||||
return format_html('<span style="color: green;">✅ {}</span>', cert.domain)
|
||||
else:
|
||||
return format_html('<span style="color: orange;">⚠️ No cert</span>')
|
||||
certificate_status.short_description = 'Certificate'
|
||||
|
||||
def certificate_info(self, obj):
|
||||
"""Display certificate information and selection logic"""
|
||||
html = '<div style="background: #f8f9fa; padding: 10px; border-radius: 4px;">'
|
||||
|
||||
if not obj.requires_certificate():
|
||||
html += '<p><strong>Certificate not required</strong> for this protocol/security combination.</p>'
|
||||
else:
|
||||
cert = obj.get_certificate()
|
||||
if obj.certificate:
|
||||
html += f'<p><strong>✅ Explicit certificate:</strong> {obj.certificate.domain}</p>'
|
||||
elif cert:
|
||||
html += f'<p><strong>🔄 Auto-selected:</strong> {cert.domain} (matches server hostname)</p>'
|
||||
else:
|
||||
server_hostname = getattr(obj.server.get_real_instance(), 'client_hostname', 'N/A')
|
||||
html += f'<p><strong>⚠️ No certificate found</strong></p>'
|
||||
html += f'<p>Server hostname: <code>{server_hostname}</code></p>'
|
||||
html += f'<p>Consider creating certificate for this domain or select one manually above.</p>'
|
||||
|
||||
html += '</div>'
|
||||
return format_html(html)
|
||||
certificate_info.short_description = 'Certificate Selection Info'
|
||||
|
||||
|
||||
# Unified Subscriptions Admin with tabs
|
||||
|
23
vpn/migrations/0024_add_certificate_to_serverinbound.py
Normal file
23
vpn/migrations/0024_add_certificate_to_serverinbound.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.7 on 2025-08-08 05:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('vpn', '0023_alter_subscriptiongroup_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='inbound',
|
||||
name='certificate',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='serverinbound',
|
||||
name='certificate',
|
||||
field=models.ForeignKey(blank=True, help_text='Certificate for TLS on this specific server (overrides automatic selection)', null=True, on_delete=django.db.models.deletion.SET_NULL, to='vpn.certificate'),
|
||||
),
|
||||
]
|
@@ -193,13 +193,6 @@ class Inbound(models.Model):
|
||||
default='none',
|
||||
help_text="Security type"
|
||||
)
|
||||
certificate = models.ForeignKey(
|
||||
Certificate,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text="Certificate for TLS"
|
||||
)
|
||||
|
||||
# Full configuration for Xray
|
||||
full_config = models.JSONField(
|
||||
@@ -328,13 +321,7 @@ class Inbound(models.Model):
|
||||
"alpn": ["h2", "http/1.1"]
|
||||
}
|
||||
|
||||
if self.certificate:
|
||||
tls_settings.update({
|
||||
"certificates": [{
|
||||
"certificateFile": f"/etc/xray/certs/{self.certificate.domain}.crt",
|
||||
"keyFile": f"/etc/xray/certs/{self.certificate.domain}.key"
|
||||
}]
|
||||
})
|
||||
# Certificate will be set during deployment based on ServerInbound configuration
|
||||
|
||||
stream_settings["tlsSettings"] = tls_settings
|
||||
|
||||
@@ -430,6 +417,15 @@ class ServerInbound(models.Model):
|
||||
deployed_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# Certificate for TLS on this specific server deployment
|
||||
certificate = models.ForeignKey(
|
||||
Certificate,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text="Certificate for TLS on this specific server (overrides automatic selection)"
|
||||
)
|
||||
|
||||
# Store deployment-specific configuration if needed
|
||||
deployment_config = models.JSONField(default=dict, blank=True, help_text="Server-specific deployment configuration")
|
||||
|
||||
@@ -441,4 +437,28 @@ class ServerInbound(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
status = "Active" if self.active else "Inactive"
|
||||
return f"{self.server.name} -> {self.inbound.name} ({status})"
|
||||
return f"{self.server.name} -> {self.inbound.name} ({status})"
|
||||
|
||||
def get_certificate(self):
|
||||
"""Get certificate for this deployment with fallback logic"""
|
||||
# 1. Use explicitly set certificate
|
||||
if self.certificate:
|
||||
return self.certificate
|
||||
|
||||
# 2. Try to find certificate by server's client_hostname
|
||||
if hasattr(self.server.get_real_instance(), 'client_hostname'):
|
||||
server_hostname = self.server.get_real_instance().client_hostname
|
||||
try:
|
||||
return Certificate.objects.get(domain=server_hostname, cert_type='letsencrypt')
|
||||
except Certificate.DoesNotExist:
|
||||
try:
|
||||
return Certificate.objects.get(domain=server_hostname)
|
||||
except Certificate.DoesNotExist:
|
||||
pass
|
||||
|
||||
# 3. No certificate found
|
||||
return None
|
||||
|
||||
def requires_certificate(self):
|
||||
"""Check if this inbound requires a certificate"""
|
||||
return self.inbound.security in ['tls'] or self.inbound.protocol == 'trojan'
|
@@ -162,7 +162,7 @@ class XrayServerV2(Server):
|
||||
logger.error(f"Failed to schedule inbound sync for server {self.name}: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def deploy_inbound(self, inbound, users=None):
|
||||
def deploy_inbound(self, inbound, users=None, server_inbound=None):
|
||||
"""Deploy a specific inbound on this server with optional users"""
|
||||
try:
|
||||
from vpn.xray_api_v2.client import XrayClient
|
||||
@@ -201,7 +201,7 @@ class XrayServerV2(Server):
|
||||
continue
|
||||
|
||||
user_configs.append(user_config)
|
||||
logger.info(f"Added user {user.username} to inbound config")
|
||||
logger.debug(f"Added user {user.username} to inbound config")
|
||||
|
||||
# Build proper inbound configuration based on protocol
|
||||
if inbound.full_config:
|
||||
@@ -213,24 +213,37 @@ class XrayServerV2(Server):
|
||||
if 'settings' not in inbound_config:
|
||||
inbound_config['settings'] = {}
|
||||
inbound_config['settings']['clients'] = user_configs
|
||||
logger.info(f"Added {len(user_configs)} users to full_config")
|
||||
logger.debug(f"Added {len(user_configs)} users to full_config")
|
||||
|
||||
# If inbound has a certificate, update the config to use inline certificates
|
||||
if inbound.certificate and inbound.certificate.certificate_pem:
|
||||
logger.info(f"Updating full_config with inline certificate for {inbound.certificate.domain}")
|
||||
# Get certificate from ServerInbound or auto-select
|
||||
certificate = None
|
||||
if server_inbound:
|
||||
certificate = server_inbound.get_certificate()
|
||||
|
||||
# If certificate found, update the config to use inline certificates
|
||||
if certificate and certificate.certificate_pem:
|
||||
logger.info(f"Updating full_config with inline certificate for {certificate.domain}")
|
||||
|
||||
# Convert PEM to lines for Xray format
|
||||
cert_lines = inbound.certificate.certificate_pem.strip().split('\n')
|
||||
key_lines = inbound.certificate.private_key_pem.strip().split('\n')
|
||||
cert_lines = certificate.certificate_pem.strip().split('\n')
|
||||
key_lines = certificate.private_key_pem.strip().split('\n')
|
||||
|
||||
# Update streamSettings if it exists
|
||||
if "streamSettings" in inbound_config and "tlsSettings" in inbound_config["streamSettings"]:
|
||||
# Remove any existing certificate file paths
|
||||
tls_settings = inbound_config["streamSettings"]["tlsSettings"]
|
||||
if "certificateFile" in tls_settings:
|
||||
del tls_settings["certificateFile"]
|
||||
if "keyFile" in tls_settings:
|
||||
del tls_settings["keyFile"]
|
||||
|
||||
# Set inline certificates
|
||||
inbound_config["streamSettings"]["tlsSettings"]["certificates"] = [{
|
||||
"certificate": cert_lines,
|
||||
"key": key_lines,
|
||||
"usage": "encipherment"
|
||||
}]
|
||||
logger.info("Updated existing tlsSettings with inline certificate")
|
||||
logger.debug("Updated existing tlsSettings with inline certificate and removed file paths")
|
||||
else:
|
||||
# Build full config based on protocol
|
||||
inbound_config = {
|
||||
@@ -281,12 +294,16 @@ class XrayServerV2(Server):
|
||||
"security": "tls"
|
||||
}
|
||||
|
||||
# Trojan always requires TLS certificate
|
||||
if inbound.certificate and inbound.certificate.certificate_pem:
|
||||
logger.info(f"Using certificate for Trojan inbound on domain {inbound.certificate.domain}")
|
||||
# Get certificate for Trojan (always required)
|
||||
certificate = None
|
||||
if server_inbound:
|
||||
certificate = server_inbound.get_certificate()
|
||||
|
||||
if certificate and certificate.certificate_pem:
|
||||
logger.info(f"Using certificate for Trojan inbound on domain {certificate.domain}")
|
||||
# Convert PEM to lines for Xray format
|
||||
cert_lines = inbound.certificate.certificate_pem.strip().split('\n')
|
||||
key_lines = inbound.certificate.private_key_pem.strip().split('\n')
|
||||
cert_lines = certificate.certificate_pem.strip().split('\n')
|
||||
key_lines = certificate.private_key_pem.strip().split('\n')
|
||||
|
||||
inbound_config["streamSettings"]["tlsSettings"] = {
|
||||
"certificates": [{
|
||||
@@ -307,12 +324,16 @@ class XrayServerV2(Server):
|
||||
inbound_config["streamSettings"] = {}
|
||||
inbound_config["streamSettings"]["security"] = "tls"
|
||||
|
||||
# Check if inbound has a certificate
|
||||
if inbound.certificate and inbound.certificate.certificate_pem:
|
||||
logger.info(f"Using certificate for domain {inbound.certificate.domain}")
|
||||
# Get certificate for TLS
|
||||
certificate = None
|
||||
if server_inbound:
|
||||
certificate = server_inbound.get_certificate()
|
||||
|
||||
if certificate and certificate.certificate_pem:
|
||||
logger.info(f"Using certificate for domain {certificate.domain}")
|
||||
# Convert PEM to lines for Xray format
|
||||
cert_lines = inbound.certificate.certificate_pem.strip().split('\n')
|
||||
key_lines = inbound.certificate.private_key_pem.strip().split('\n')
|
||||
cert_lines = certificate.certificate_pem.strip().split('\n')
|
||||
key_lines = certificate.private_key_pem.strip().split('\n')
|
||||
|
||||
inbound_config["streamSettings"]["tlsSettings"] = {
|
||||
"certificates": [{
|
||||
@@ -327,7 +348,7 @@ class XrayServerV2(Server):
|
||||
"certificates": []
|
||||
}
|
||||
|
||||
logger.info(f"Inbound config: {inbound_config}")
|
||||
logger.debug(f"Inbound config for {inbound.name}: {len(str(inbound_config))} chars")
|
||||
|
||||
# Add inbound using the client's add_inbound method which handles wrapping
|
||||
try:
|
||||
@@ -360,14 +381,22 @@ class XrayServerV2(Server):
|
||||
"""Add a user to a specific inbound on this server using inbound recreation approach"""
|
||||
try:
|
||||
from vpn.xray_api_v2.client import XrayClient
|
||||
from vpn.models_xray import ServerInbound
|
||||
import uuid
|
||||
|
||||
logger.info(f"Adding user {user.username} to inbound {inbound.name} using inbound recreation")
|
||||
client = XrayClient(server=self.api_address)
|
||||
|
||||
# Get ServerInbound object for certificate access
|
||||
try:
|
||||
server_inbound = ServerInbound.objects.get(server=self, inbound=inbound)
|
||||
except ServerInbound.DoesNotExist:
|
||||
logger.warning(f"ServerInbound not found for {self.name} -> {inbound.name}, creating one")
|
||||
server_inbound = ServerInbound.objects.create(server=self, inbound=inbound, active=True)
|
||||
|
||||
# Generate user UUID based on username and inbound
|
||||
user_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{user.username}-{inbound.name}"))
|
||||
logger.info(f"Generated UUID for user {user.username}: {user_uuid}")
|
||||
logger.debug(f"Generated UUID for user {user.username}: {user_uuid}")
|
||||
|
||||
# Build user config based on protocol
|
||||
if inbound.protocol == 'vless':
|
||||
@@ -406,8 +435,13 @@ class XrayServerV2(Server):
|
||||
|
||||
if not existing_inbound:
|
||||
logger.warning(f"Inbound {inbound.name} not found on server, deploying it first")
|
||||
# Get or create ServerInbound for certificate access
|
||||
from vpn.models_xray import ServerInbound
|
||||
server_inbound_obj, created = ServerInbound.objects.get_or_create(
|
||||
server=self, inbound=inbound, defaults={'active': True}
|
||||
)
|
||||
# Deploy the inbound if it doesn't exist
|
||||
if not self.deploy_inbound(inbound):
|
||||
if not self.deploy_inbound(inbound, server_inbound=server_inbound_obj):
|
||||
logger.error(f"Failed to deploy inbound {inbound.name}")
|
||||
return False
|
||||
# Get the inbound config we just created
|
||||
@@ -439,11 +473,22 @@ class XrayServerV2(Server):
|
||||
inbound_config['settings']['clients'] = existing_users
|
||||
|
||||
# Handle certificate embedding if needed
|
||||
if inbound.certificate and inbound.certificate.certificate_pem:
|
||||
cert_lines = inbound.certificate.certificate_pem.strip().split('\n')
|
||||
key_lines = inbound.certificate.private_key_pem.strip().split('\n')
|
||||
certificate = None
|
||||
if server_inbound:
|
||||
certificate = server_inbound.get_certificate()
|
||||
|
||||
if certificate and certificate.certificate_pem:
|
||||
cert_lines = certificate.certificate_pem.strip().split('\n')
|
||||
key_lines = certificate.private_key_pem.strip().split('\n')
|
||||
|
||||
if "streamSettings" in inbound_config and "tlsSettings" in inbound_config["streamSettings"]:
|
||||
# Remove any existing certificate file paths
|
||||
tls_settings = inbound_config["streamSettings"]["tlsSettings"]
|
||||
if "certificateFile" in tls_settings:
|
||||
del tls_settings["certificateFile"]
|
||||
if "keyFile" in tls_settings:
|
||||
del tls_settings["keyFile"]
|
||||
|
||||
inbound_config["streamSettings"]["tlsSettings"]["certificates"] = [{
|
||||
"certificate": cert_lines,
|
||||
"key": key_lines,
|
||||
@@ -611,19 +656,16 @@ class XrayServerV2(Server):
|
||||
# Check if inbound exists on server
|
||||
if inbound.name not in existing_inbound_tags:
|
||||
logger.info(f"Inbound {inbound.name} doesn't exist on server, creating with user")
|
||||
# Get or create ServerInbound for certificate access
|
||||
from vpn.models_xray import ServerInbound
|
||||
server_inbound_obj, created = ServerInbound.objects.get_or_create(
|
||||
server=self, inbound=inbound, defaults={'active': True}
|
||||
)
|
||||
# Create the inbound with the user directly
|
||||
if self.deploy_inbound(inbound, users=[user]):
|
||||
if self.deploy_inbound(inbound, users=[user], server_inbound=server_inbound_obj):
|
||||
logger.info(f"Successfully created inbound {inbound.name} with user {user.username}")
|
||||
added_count += 1
|
||||
existing_inbound_tags.add(inbound.name)
|
||||
|
||||
# Mark as deployed on this server
|
||||
from vpn.models_xray import ServerInbound
|
||||
ServerInbound.objects.update_or_create(
|
||||
server=self,
|
||||
inbound=inbound,
|
||||
defaults={'active': True}
|
||||
)
|
||||
else:
|
||||
logger.error(f"Failed to create inbound {inbound.name} with user")
|
||||
continue
|
||||
@@ -685,9 +727,17 @@ class ServerInboundInline(admin.TabularInline):
|
||||
from vpn.models_xray import ServerInbound
|
||||
model = ServerInbound
|
||||
extra = 0
|
||||
fields = ('inbound', 'active')
|
||||
fields = ('inbound', 'certificate', 'active')
|
||||
verbose_name = "Inbound Template"
|
||||
verbose_name_plural = "Inbound Templates"
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
"""Filter certificates for inbound selection"""
|
||||
if db_field.name == 'certificate':
|
||||
from vpn.models_xray import Certificate
|
||||
kwargs['queryset'] = Certificate.objects.filter(cert_type__in=['letsencrypt', 'custom'])
|
||||
kwargs['empty_label'] = "Auto-select by server hostname"
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
|
||||
class XrayServerV2Admin(admin.ModelAdmin):
|
||||
@@ -697,6 +747,10 @@ class XrayServerV2Admin(admin.ModelAdmin):
|
||||
readonly_fields = ['server_type', 'registration_date']
|
||||
inlines = [ServerInboundInline]
|
||||
|
||||
def has_module_permission(self, request):
|
||||
"""Hide this model from the main admin index"""
|
||||
return False
|
||||
|
||||
fieldsets = [
|
||||
('Basic Information', {
|
||||
'fields': ('name', 'comment', 'server_type')
|
||||
|
Reference in New Issue
Block a user