Explorar el Código

feat(vpn): support PPP profiles for PPPoE, L2TP, and PPTP clients

Proxmox Router hace 1 semana
padre
commit
64b8320e10

+ 13 - 0
internal/vpn/l2tp.go

@@ -73,6 +73,9 @@ func (m *L2TPManager) Save(inst L2TPInstance) error {
 	if inst.ConnectTo == "" {
 		return fmt.Errorf("server address (connect-to) is required")
 	}
+	if inst.Profile == "" {
+		inst.Profile = "default-encryption"
+	}
 
 	found := false
 	for i, existing := range m.instances {
@@ -161,6 +164,10 @@ func (m *L2TPManager) regenerateXl2tpdConf() error {
 	sb.WriteString("port = 1701\n\n")
 
 	for _, inst := range m.instances {
+		prof := inst.Profile
+		if prof == "" {
+			prof = "default-encryption"
+		}
 		optFile := fmt.Sprintf("/etc/ppp/options.l2tpd.%s", inst.Name)
 		sb.WriteString(fmt.Sprintf("[lac %s]\n", inst.Name))
 		sb.WriteString(fmt.Sprintf("lns = %s\n", inst.ConnectTo))
@@ -171,12 +178,18 @@ func (m *L2TPManager) regenerateXl2tpdConf() error {
 
 		// Create individual options file
 		var optSb strings.Builder
+		optSb.WriteString(fmt.Sprintf("# L2TP Client: %s (Profile: %s)\n", inst.Name, prof))
 		optSb.WriteString(fmt.Sprintf("name %q\n", inst.Username))
 		optSb.WriteString(fmt.Sprintf("password %q\n", inst.Password))
 		optSb.WriteString("ipcp-accept-local\n")
 		optSb.WriteString("ipcp-accept-remote\n")
 		optSb.WriteString("refuse-eap\n")
 		optSb.WriteString("require-mschap-v2\n")
+		if strings.EqualFold(prof, "default") {
+			optSb.WriteString("nomppe\n")
+		} else {
+			optSb.WriteString("require-mppe-128\n")
+		}
 		optSb.WriteString("noccp\n")
 		optSb.WriteString("noauth\n")
 		optSb.WriteString("mtu 1410\n")

+ 3 - 0
internal/vpn/pppoe.go

@@ -87,6 +87,9 @@ func (m *PPPoEManager) Save(inst PPPoEInstance) error {
 	if inst.MTU <= 0 {
 		inst.MTU = 1492
 	}
+	if inst.Profile == "" {
+		inst.Profile = "default"
+	}
 
 	found := false
 	for i, existing := range m.instances {

+ 12 - 2
internal/vpn/pptp.go

@@ -71,6 +71,9 @@ func (m *PPTPManager) Save(inst PPTPInstance) error {
 	if inst.ConnectTo == "" {
 		return fmt.Errorf("server address (connect-to) is required")
 	}
+	if inst.Profile == "" {
+		inst.Profile = "default-encryption"
+	}
 
 	found := false
 	for i, existing := range m.instances {
@@ -169,12 +172,19 @@ func (m *PPTPManager) disconnectInstance(name string) error {
 func (m *PPTPManager) generatePeerConfig(inst PPTPInstance) error {
 	_ = os.MkdirAll("/etc/ppp/peers", 0755)
 
+	prof := inst.Profile
+	if prof == "" {
+		prof = "default-encryption"
+	}
+
 	var sb strings.Builder
-	sb.WriteString(fmt.Sprintf("# MikroTik-Style PPTP Client: %s\n", inst.Name))
+	sb.WriteString(fmt.Sprintf("# MikroTik-Style PPTP Client: %s (Profile: %s)\n", inst.Name, prof))
 	sb.WriteString(fmt.Sprintf("pty \"pptp %s --nolaunchpppd\"\n", inst.ConnectTo))
 	sb.WriteString(fmt.Sprintf("name %q\n", inst.Username))
 	sb.WriteString("remotename PPTP\n")
-	if inst.RequireMPPE {
+	if strings.EqualFold(prof, "default") && !inst.RequireMPPE {
+		sb.WriteString("nomppe\n")
+	} else {
 		sb.WriteString("require-mppe-128\n")
 	}
 	if inst.DefaultRoute {

+ 3 - 0
internal/vpn/types.go

@@ -29,6 +29,7 @@ type PPPoEInstance struct {
 	Interface     string    `json:"interface"`      // Physical or VLAN interface, e.g. "eth0", "eth0.100"
 	Username      string    `json:"username"`
 	Password      string    `json:"password"`
+	Profile       string    `json:"profile,omitempty"` // PPP Profile from /ppp profile (default "default")
 	ServiceName   string    `json:"service_name,omitempty"`
 	MTU           int       `json:"mtu"`            // default 1492
 	DefaultRoute  bool      `json:"default_route"`
@@ -92,6 +93,7 @@ type PPTPInstance struct {
 	ConnectTo     string `json:"connect_to"`     // Server IP / hostname
 	Username      string `json:"username"`
 	Password      string `json:"password"`
+	Profile       string `json:"profile,omitempty"` // PPP Profile from /ppp profile (default "default-encryption")
 	RequireMPPE   bool   `json:"require_mppe"`
 	DefaultRoute  bool   `json:"default_route"`
 	AutoReconnect bool   `json:"auto_reconnect"`
@@ -108,6 +110,7 @@ type L2TPInstance struct {
 	ConnectTo     string `json:"connect_to"`     // Server IP / hostname
 	Username      string `json:"username"`
 	Password      string `json:"password"`
+	Profile       string `json:"profile,omitempty"` // PPP Profile from /ppp profile (default "default-encryption")
 	IPsecSecret   string `json:"ipsec_secret,omitempty"` // For L2TP/IPsec
 	DefaultRoute  bool   `json:"default_route"`
 	Disabled      bool   `json:"disabled"`

+ 96 - 25
internal/web/static/app.js

@@ -976,9 +976,51 @@ function copyToClipboard(text, label = 'Text') {
   });
 }
 
+// ======================== PPP Profiles Shared Helper ========================
+async function fetchPPPProfilesList() {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/ppp/profiles`);
+    if (res.ok) {
+      state.pppProfiles = await res.json();
+    }
+  } catch (err) {
+    console.error('Failed to load PPP profiles:', err);
+  }
+}
+
+function populateProfileSelect(selectId, selectedVal, defaultVal = 'default') {
+  const sel = document.getElementById(selectId);
+  if (!sel) return;
+  const profiles = (state.pppProfiles && state.pppProfiles.length > 0) ? state.pppProfiles : [
+    { name: 'default', use_encryption: 'no' },
+    { name: 'default-encryption', use_encryption: 'required' }
+  ];
+
+  sel.innerHTML = '';
+  profiles.forEach(p => {
+    const opt = document.createElement('option');
+    opt.value = p.name;
+    const encBadge = p.use_encryption === 'required' ? ' (MPPE-128)' : (p.use_encryption === 'yes' ? ' (MPPE)' : '');
+    opt.innerText = `${p.name}${encBadge}`;
+    sel.appendChild(opt);
+  });
+
+  if (selectedVal) {
+    sel.value = selectedVal;
+  } else {
+    sel.value = defaultVal;
+  }
+  if (!sel.value && sel.options.length > 0) {
+    sel.selectedIndex = 0;
+  }
+}
+
 // ======================== PPPoE Multi-Instance ========================
 async function loadPPPoE(populateInputs = true) {
   try {
+    if (!state.pppProfiles || state.pppProfiles.length === 0) {
+      await fetchPPPProfilesList();
+    }
     const res = await fetch(`${API_BASE}/vpn/pppoe/clients`);
     const list = await res.json();
     state.pppoeList = Array.isArray(list) ? list : [];
@@ -1000,11 +1042,12 @@ async function loadPPPoE(populateInputs = true) {
         tr.className = 'hover:bg-slate-800/40 transition text-xs';
         tr.innerHTML = `
           <td class="py-2.5 px-3 text-center">${renderMikroTikFlag(inst.running, inst.disabled)}</td>
-          <td class="py-2.5 px-3 font-bold text-white">${inst.name}</td>
-          <td class="py-2.5 px-3 text-cyan-400 font-mono">${inst.interface}</td>
-          <td class="py-2.5 px-3 text-slate-300 font-mono">${inst.username || '-'}</td>
-          <td class="py-2.5 px-3 font-mono ${inst.assigned_ip ? 'text-emerald-400 font-bold' : 'text-slate-500'}">${inst.assigned_ip || '-'}</td>
-          <td class="py-2.5 px-3 text-slate-400 font-mono">${inst.ppp_device || '-'}</td>
+          <td class="py-2.5 px-3 font-bold text-white">${escapeHtml(inst.name)}</td>
+          <td class="py-2.5 px-3 text-cyan-400 font-mono">${escapeHtml(inst.interface)}</td>
+          <td class="py-2.5 px-3 text-slate-300 font-mono">${escapeHtml(inst.username || '-')}</td>
+          <td class="py-2.5 px-3 font-mono text-cyan-300"><span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 border border-slate-700">${escapeHtml(inst.profile || 'default')}</span></td>
+          <td class="py-2.5 px-3 font-mono ${inst.assigned_ip ? 'text-emerald-400 font-bold' : 'text-slate-500'}">${escapeHtml(inst.assigned_ip || '-')}</td>
+          <td class="py-2.5 px-3 text-slate-400 font-mono">${escapeHtml(inst.ppp_device || '-')}</td>
           <td class="py-2.5 px-3 text-center">
             <span class="px-2 py-0.5 rounded text-[10px] font-sans ${inst.default_route ? 'bg-blue-500/20 text-blue-300 border border-blue-500/30' : 'bg-slate-800 text-slate-500'}">
               ${inst.default_route ? 'Yes' : 'No'}
@@ -1044,6 +1087,9 @@ function openAddPPPoEModal() {
   document.getElementById('pppoe-modal-reconnect').checked = true;
   document.getElementById('pppoe-modal-disabled').checked = false;
 
+  // Populate profiles
+  populateProfileSelect('pppoe-modal-profile', 'default', 'default');
+
   // Populate interfaces
   const sel = document.getElementById('pppoe-modal-iface');
   sel.innerHTML = '';
@@ -1074,6 +1120,9 @@ function openEditPPPoEModal(name) {
   document.getElementById('pppoe-modal-reconnect').checked = inst.auto_reconnect ?? true;
   document.getElementById('pppoe-modal-disabled').checked = !!inst.disabled;
 
+  // Populate profiles
+  populateProfileSelect('pppoe-modal-profile', inst.profile || 'default', 'default');
+
   const sel = document.getElementById('pppoe-modal-iface');
   sel.innerHTML = '';
   (state.interfaces || []).forEach(i => {
@@ -1098,6 +1147,7 @@ async function submitSavePPPoE(e) {
   const iface = document.getElementById('pppoe-modal-iface').value;
   const user = document.getElementById('pppoe-modal-user').value.trim();
   const pass = document.getElementById('pppoe-modal-pass').value;
+  const profile = document.getElementById('pppoe-modal-profile')?.value || 'default';
   const service = document.getElementById('pppoe-modal-service').value.trim();
   const mtu = parseInt(document.getElementById('pppoe-modal-mtu').value, 10) || 1492;
   const defRoute = document.getElementById('pppoe-modal-defroute').checked;
@@ -1113,6 +1163,7 @@ async function submitSavePPPoE(e) {
         interface: iface,
         username: user,
         password: pass,
+        profile: profile,
         service_name: service,
         mtu,
         default_route: defRoute,
@@ -2219,6 +2270,9 @@ async function deleteIPSecConn(name) {
 // ======================== L2TP Multi-Instance ========================
 async function loadL2TP(populateInputs = true) {
   try {
+    if (!state.pppProfiles || state.pppProfiles.length === 0) {
+      await fetchPPPProfilesList();
+    }
     const res = await fetch(`${API_BASE}/vpn/l2tp/clients`);
     const list = await res.json();
     state.l2tpList = Array.isArray(list) ? list : [];
@@ -2240,11 +2294,12 @@ async function loadL2TP(populateInputs = true) {
         tr.className = 'hover:bg-slate-800/40 transition text-xs';
         tr.innerHTML = `
           <td class="py-2.5 px-3 text-center">${renderMikroTikFlag(inst.running, inst.disabled)}</td>
-          <td class="py-2.5 px-3 font-bold text-white">${inst.name}</td>
-          <td class="py-2.5 px-3 text-cyan-400 font-mono">${inst.connect_to}</td>
-          <td class="py-2.5 px-3 text-slate-300 font-mono">${inst.username || '-'}</td>
-          <td class="py-2.5 px-3 font-mono ${inst.assigned_ip ? 'text-emerald-400 font-bold' : 'text-slate-500'}">${inst.assigned_ip || '-'}</td>
-          <td class="py-2.5 px-3 text-slate-400 font-mono">${inst.ppp_device || '-'}</td>
+          <td class="py-2.5 px-3 font-bold text-white">${escapeHtml(inst.name)}</td>
+          <td class="py-2.5 px-3 text-cyan-400 font-mono">${escapeHtml(inst.connect_to)}</td>
+          <td class="py-2.5 px-3 text-slate-300 font-mono">${escapeHtml(inst.username || '-')}</td>
+          <td class="py-2.5 px-3 font-mono text-cyan-300"><span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 border border-slate-700">${escapeHtml(inst.profile || 'default-encryption')}</span></td>
+          <td class="py-2.5 px-3 font-mono ${inst.assigned_ip ? 'text-emerald-400 font-bold' : 'text-slate-500'}">${escapeHtml(inst.assigned_ip || '-')}</td>
+          <td class="py-2.5 px-3 text-slate-400 font-mono">${escapeHtml(inst.ppp_device || '-')}</td>
           <td class="py-2.5 px-3 text-center">
             <span class="px-2 py-0.5 rounded text-[10px] font-sans ${inst.default_route ? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/30' : 'bg-slate-800 text-slate-500'}">
               ${inst.default_route ? 'Yes' : 'No'}
@@ -2252,15 +2307,15 @@ async function loadL2TP(populateInputs = true) {
           </td>
           <td class="py-2.5 px-3 text-right space-x-2 font-sans">
             ${inst.running ? `
-              <button onclick="disconnectL2TPInstance('${inst.name}')" class="text-xs text-rose-400 hover:text-rose-300">Disconnect</button>
+              <button onclick="disconnectL2TPInstance('${escapeHtml(inst.name)}')" class="text-xs text-rose-400 hover:text-rose-300">Disconnect</button>
             ` : `
-              <button onclick="connectL2TPInstance('${inst.name}')" class="text-xs bg-cyan-600/30 hover:bg-cyan-600/50 text-cyan-300 px-2 py-0.5 rounded border border-cyan-500/30">Connect</button>
+              <button onclick="connectL2TPInstance('${escapeHtml(inst.name)}')" class="text-xs bg-cyan-600/30 hover:bg-cyan-600/50 text-cyan-300 px-2 py-0.5 rounded border border-cyan-500/30">Connect</button>
             `}
-            <button onclick="toggleL2TPState('${inst.name}', ${inst.disabled ? 'true' : 'false'})" class="text-xs text-slate-400 hover:text-white">
+            <button onclick="toggleL2TPState('${escapeHtml(inst.name)}', ${inst.disabled ? 'true' : 'false'})" class="text-xs text-slate-400 hover:text-white">
               ${inst.disabled ? 'Enable' : 'Disable'}
             </button>
-            <button onclick="openEditL2TPModal('${inst.name}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
-            <button onclick="deleteL2TPInstance('${inst.name}')" class="text-xs text-slate-500 hover:text-rose-400">Delete</button>
+            <button onclick="openEditL2TPModal('${escapeHtml(inst.name)}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
+            <button onclick="deleteL2TPInstance('${escapeHtml(inst.name)}')" class="text-xs text-slate-500 hover:text-rose-400">Delete</button>
           </td>
         `;
         tbody.appendChild(tr);
@@ -2282,6 +2337,8 @@ function openAddL2TPModal() {
   document.getElementById('l2tp-modal-defroute').checked = true;
   document.getElementById('l2tp-modal-disabled').checked = false;
 
+  populateProfileSelect('l2tp-modal-profile', 'default-encryption', 'default-encryption');
+
   document.getElementById('l2tp-modal').classList.remove('hidden');
 }
 
@@ -2299,6 +2356,8 @@ function openEditL2TPModal(name) {
   document.getElementById('l2tp-modal-defroute').checked = inst.default_route ?? true;
   document.getElementById('l2tp-modal-disabled').checked = !!inst.disabled;
 
+  populateProfileSelect('l2tp-modal-profile', inst.profile || 'default-encryption', 'default-encryption');
+
   document.getElementById('l2tp-modal').classList.remove('hidden');
 }
 
@@ -2312,6 +2371,7 @@ async function submitSaveL2TP(e) {
   const server = document.getElementById('l2tp-modal-server').value.trim();
   const user = document.getElementById('l2tp-modal-user').value.trim();
   const pass = document.getElementById('l2tp-modal-pass').value;
+  const profile = document.getElementById('l2tp-modal-profile')?.value || 'default-encryption';
   const ipsec = document.getElementById('l2tp-modal-ipsec').value;
   const defRoute = document.getElementById('l2tp-modal-defroute').checked;
   const disabled = document.getElementById('l2tp-modal-disabled').checked;
@@ -2325,6 +2385,7 @@ async function submitSaveL2TP(e) {
         connect_to: server,
         username: user,
         password: pass,
+        profile: profile,
         ipsec_secret: ipsec,
         default_route: defRoute,
         disabled
@@ -2398,6 +2459,9 @@ async function deleteL2TPInstance(name) {
 // ======================== PPTP Multi-Instance ========================
 async function loadPPTP(populateInputs = true) {
   try {
+    if (!state.pppProfiles || state.pppProfiles.length === 0) {
+      await fetchPPPProfilesList();
+    }
     const res = await fetch(`${API_BASE}/vpn/pptp/clients`);
     const list = await res.json();
     state.pptpList = Array.isArray(list) ? list : [];
@@ -2419,11 +2483,12 @@ async function loadPPTP(populateInputs = true) {
         tr.className = 'hover:bg-slate-800/40 transition text-xs';
         tr.innerHTML = `
           <td class="py-2.5 px-3 text-center">${renderMikroTikFlag(inst.running, inst.disabled)}</td>
-          <td class="py-2.5 px-3 font-bold text-white">${inst.name}</td>
-          <td class="py-2.5 px-3 text-purple-400 font-mono">${inst.connect_to}</td>
-          <td class="py-2.5 px-3 text-slate-300 font-mono">${inst.username || '-'}</td>
-          <td class="py-2.5 px-3 font-mono ${inst.assigned_ip ? 'text-emerald-400 font-bold' : 'text-slate-500'}">${inst.assigned_ip || '-'}</td>
-          <td class="py-2.5 px-3 text-slate-400 font-mono">${inst.ppp_device || '-'}</td>
+          <td class="py-2.5 px-3 font-bold text-white">${escapeHtml(inst.name)}</td>
+          <td class="py-2.5 px-3 text-purple-400 font-mono">${escapeHtml(inst.connect_to)}</td>
+          <td class="py-2.5 px-3 text-slate-300 font-mono">${escapeHtml(inst.username || '-')}</td>
+          <td class="py-2.5 px-3 font-mono text-purple-300"><span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 border border-slate-700">${escapeHtml(inst.profile || 'default-encryption')}</span></td>
+          <td class="py-2.5 px-3 font-mono ${inst.assigned_ip ? 'text-emerald-400 font-bold' : 'text-slate-500'}">${escapeHtml(inst.assigned_ip || '-')}</td>
+          <td class="py-2.5 px-3 text-slate-400 font-mono">${escapeHtml(inst.ppp_device || '-')}</td>
           <td class="py-2.5 px-3 text-center">
             <span class="px-2 py-0.5 rounded text-[10px] font-sans ${inst.require_mppe ? 'bg-purple-500/20 text-purple-300 border border-purple-500/30' : 'bg-slate-800 text-slate-500'}">
               ${inst.require_mppe ? 'MPPE-128' : 'None'}
@@ -2431,15 +2496,15 @@ async function loadPPTP(populateInputs = true) {
           </td>
           <td class="py-2.5 px-3 text-right space-x-2 font-sans">
             ${inst.running ? `
-              <button onclick="disconnectPPTPInstance('${inst.name}')" class="text-xs text-rose-400 hover:text-rose-300">Disconnect</button>
+              <button onclick="disconnectPPTPInstance('${escapeHtml(inst.name)}')" class="text-xs text-rose-400 hover:text-rose-300">Disconnect</button>
             ` : `
-              <button onclick="connectPPTPInstance('${inst.name}')" class="text-xs bg-purple-600/30 hover:bg-purple-600/50 text-purple-300 px-2 py-0.5 rounded border border-purple-500/30">Connect</button>
+              <button onclick="connectPPTPInstance('${escapeHtml(inst.name)}')" class="text-xs bg-purple-600/30 hover:bg-purple-600/50 text-purple-300 px-2 py-0.5 rounded border border-purple-500/30">Connect</button>
             `}
-            <button onclick="togglePPTPState('${inst.name}', ${inst.disabled ? 'true' : 'false'})" class="text-xs text-slate-400 hover:text-white">
+            <button onclick="togglePPTPState('${escapeHtml(inst.name)}', ${inst.disabled ? 'true' : 'false'})" class="text-xs text-slate-400 hover:text-white">
               ${inst.disabled ? 'Enable' : 'Disable'}
             </button>
-            <button onclick="openEditPPTPModal('${inst.name}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
-            <button onclick="deletePPTPInstance('${inst.name}')" class="text-xs text-slate-500 hover:text-rose-400">Delete</button>
+            <button onclick="openEditPPTPModal('${escapeHtml(inst.name)}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
+            <button onclick="deletePPTPInstance('${escapeHtml(inst.name)}')" class="text-xs text-slate-500 hover:text-rose-400">Delete</button>
           </td>
         `;
         tbody.appendChild(tr);
@@ -2461,6 +2526,8 @@ function openAddPPTPModal() {
   document.getElementById('pptp-modal-defroute').checked = true;
   document.getElementById('pptp-modal-disabled').checked = false;
 
+  populateProfileSelect('pptp-modal-profile', 'default-encryption', 'default-encryption');
+
   document.getElementById('pptp-modal').classList.remove('hidden');
 }
 
@@ -2478,6 +2545,8 @@ function openEditPPTPModal(name) {
   document.getElementById('pptp-modal-defroute').checked = inst.default_route ?? true;
   document.getElementById('pptp-modal-disabled').checked = !!inst.disabled;
 
+  populateProfileSelect('pptp-modal-profile', inst.profile || 'default-encryption', 'default-encryption');
+
   document.getElementById('pptp-modal').classList.remove('hidden');
 }
 
@@ -2491,6 +2560,7 @@ async function submitSavePPTP(e) {
   const server = document.getElementById('pptp-modal-server').value.trim();
   const user = document.getElementById('pptp-modal-user').value.trim();
   const pass = document.getElementById('pptp-modal-pass').value;
+  const profile = document.getElementById('pptp-modal-profile')?.value || 'default-encryption';
   const mppe = document.getElementById('pptp-modal-mppe').checked;
   const defRoute = document.getElementById('pptp-modal-defroute').checked;
   const disabled = document.getElementById('pptp-modal-disabled').checked;
@@ -2504,6 +2574,7 @@ async function submitSavePPTP(e) {
         connect_to: server,
         username: user,
         password: pass,
+        profile: profile,
         require_mppe: mppe,
         default_route: defRoute,
         auto_reconnect: true,

+ 31 - 5
internal/web/static/index.html

@@ -876,6 +876,7 @@
                     <th class="py-2.5 px-3">Name</th>
                     <th class="py-2.5 px-3">Interface</th>
                     <th class="py-2.5 px-3">User</th>
+                    <th class="py-2.5 px-3">Profile</th>
                     <th class="py-2.5 px-3">Assigned IP</th>
                     <th class="py-2.5 px-3">Device</th>
                     <th class="py-2.5 px-3 text-center">Def. Route</th>
@@ -1078,6 +1079,7 @@
                     <th class="py-2.5 px-3">Name</th>
                     <th class="py-2.5 px-3">Server Address</th>
                     <th class="py-2.5 px-3">User</th>
+                    <th class="py-2.5 px-3">Profile</th>
                     <th class="py-2.5 px-3">Assigned IP</th>
                     <th class="py-2.5 px-3">Device</th>
                     <th class="py-2.5 px-3 text-center">Def. Route</th>
@@ -1128,6 +1130,7 @@
                     <th class="py-2.5 px-3">Name</th>
                     <th class="py-2.5 px-3">Server Address</th>
                     <th class="py-2.5 px-3">User</th>
+                    <th class="py-2.5 px-3">Profile</th>
                     <th class="py-2.5 px-3">Assigned IP</th>
                     <th class="py-2.5 px-3">Device</th>
                     <th class="py-2.5 px-3 text-center">MPPE</th>
@@ -3417,10 +3420,17 @@
             <input type="text" id="pppoe-modal-service" placeholder="ISP Service" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white">
           </div>
           <div>
-            <label class="block text-xs font-semibold text-slate-300 mb-1.5">MTU</label>
-            <input type="number" id="pppoe-modal-mtu" value="1492" min="576" max="1500" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono">
+            <label class="block text-xs font-semibold text-slate-300 mb-1.5">PPP Profile</label>
+            <select id="pppoe-modal-profile" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono focus:outline-none focus:border-blue-500">
+              <option value="default" selected>default</option>
+              <option value="default-encryption">default-encryption</option>
+            </select>
           </div>
         </div>
+        <div>
+          <label class="block text-xs font-semibold text-slate-300 mb-1.5">MTU</label>
+          <input type="number" id="pppoe-modal-mtu" value="1492" min="576" max="1500" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono">
+        </div>
         <div class="grid grid-cols-3 gap-2 pt-2 border-t border-slate-800/80">
           <label class="flex items-center space-x-2 cursor-pointer">
             <input type="checkbox" id="pppoe-modal-defroute" checked class="rounded bg-slate-800 border-slate-700 text-blue-500">
@@ -3892,9 +3902,18 @@
             <input type="password" id="l2tp-modal-pass" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono focus:outline-none focus:border-cyan-500">
           </div>
         </div>
-        <div>
-          <label class="block text-xs font-semibold text-slate-300 mb-1.5">IPSec Secret / PSK (Optional for L2TP/IPsec)</label>
-          <input type="password" id="l2tp-modal-ipsec" placeholder="Optional Pre-Shared Key" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono">
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block text-xs font-semibold text-slate-300 mb-1.5">PPP Profile</label>
+            <select id="l2tp-modal-profile" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono focus:outline-none focus:border-cyan-500">
+              <option value="default-encryption" selected>default-encryption (MPPE-128)</option>
+              <option value="default">default (No encryption)</option>
+            </select>
+          </div>
+          <div>
+            <label class="block text-xs font-semibold text-slate-300 mb-1.5">IPSec Secret / PSK (Optional)</label>
+            <input type="password" id="l2tp-modal-ipsec" placeholder="Optional Pre-Shared Key" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono">
+          </div>
         </div>
         <div class="grid grid-cols-2 gap-4 pt-2">
           <label class="flex items-center space-x-2 cursor-pointer">
@@ -3940,6 +3959,13 @@
             <input type="password" id="pptp-modal-pass" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono focus:outline-none focus:border-purple-500">
           </div>
         </div>
+        <div>
+          <label class="block text-xs font-semibold text-slate-300 mb-1.5">PPP Profile</label>
+          <select id="pptp-modal-profile" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono focus:outline-none focus:border-purple-500">
+            <option value="default-encryption" selected>default-encryption (MPPE-128)</option>
+            <option value="default">default (No encryption)</option>
+          </select>
+        </div>
         <div class="grid grid-cols-3 gap-2 pt-2 border-t border-slate-800/80">
           <label class="flex items-center space-x-2 cursor-pointer">
             <input type="checkbox" id="pptp-modal-mppe" checked class="rounded bg-slate-800 border-slate-700 text-purple-500">