package vpn import ( "encoding/json" "fmt" "net" "os" "os/exec" "path/filepath" "strings" "sync" "syscall" "github.com/vishvananda/netlink" ) const ( L2TPInstancesStorePath = "/etc/debtik/vpn/l2tp_instances.json" Xl2tpdConfigPath = "/etc/xl2tpd/xl2tpd.conf" L2TPControlFIFO = "/var/run/xl2tpd/l2tp-control" ) type L2TPManager struct { mu sync.Mutex instances []L2TPInstance pppServer *PPPServerManager } func NewL2TPManager() *L2TPManager { m := &L2TPManager{ instances: make([]L2TPInstance, 0), } m.load() return m } func (m *L2TPManager) SetPPPServer(ps *PPPServerManager) { m.pppServer = ps } func (m *L2TPManager) load() { data, err := os.ReadFile(L2TPInstancesStorePath) if err == nil { _ = json.Unmarshal(data, &m.instances) } if m.instances == nil { m.instances = make([]L2TPInstance, 0) } } func (m *L2TPManager) save() error { _ = os.MkdirAll(filepath.Dir(L2TPInstancesStorePath), 0755) data, err := json.MarshalIndent(m.instances, "", " ") if err != nil { return err } return os.WriteFile(L2TPInstancesStorePath, data, 0600) } func (m *L2TPManager) List() []L2TPInstance { m.mu.Lock() defer m.mu.Unlock() m.refreshRuntimeStatus() res := make([]L2TPInstance, len(m.instances)) copy(res, m.instances) return res } func (m *L2TPManager) Save(inst L2TPInstance) error { m.mu.Lock() defer m.mu.Unlock() if inst.Name == "" { return fmt.Errorf("instance name is required (e.g. l2tp-out1)") } 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 { if existing.Name == inst.Name { m.instances[i] = inst found = true break } } if !found { m.instances = append(m.instances, inst) } if err := m.save(); err != nil { return err } if err := m.regenerateXl2tpdConf(); err != nil { return err } if !inst.Disabled { return m.connectInstance(inst.Name) } else { return m.disconnectInstance(inst.Name) } } func (m *L2TPManager) Delete(name string) error { m.mu.Lock() defer m.mu.Unlock() _ = m.disconnectInstance(name) updated := make([]L2TPInstance, 0) for _, inst := range m.instances { if inst.Name != name { updated = append(updated, inst) } } m.instances = updated if err := m.save(); err != nil { return err } return m.regenerateXl2tpdConf() } func (m *L2TPManager) SetEnabled(name string, enabled bool) error { m.mu.Lock() defer m.mu.Unlock() for i, inst := range m.instances { if inst.Name == name { m.instances[i].Disabled = !enabled if err := m.save(); err != nil { return err } if enabled { return m.connectInstance(name) } else { return m.disconnectInstance(name) } } } return fmt.Errorf("instance %q not found", name) } func (m *L2TPManager) Connect(name string) error { m.mu.Lock() defer m.mu.Unlock() return m.connectInstance(name) } func (m *L2TPManager) Disconnect(name string) error { m.mu.Lock() defer m.mu.Unlock() return m.disconnectInstance(name) } func (m *L2TPManager) regenerateXl2tpdConf() error { _ = os.MkdirAll("/etc/xl2tpd", 0755) _ = os.MkdirAll("/var/run/xl2tpd", 0755) var sb strings.Builder sb.WriteString("[global]\n") sb.WriteString("port = 1701\n\n") for _, inst := range m.instances { prof := inst.Profile if prof == "" { prof = "default-encryption" } var profObj *PPPProfile if m.pppServer != nil { profObj = m.pppServer.GetProfile(prof) } 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)) sb.WriteString("ppp debug = yes\n") sb.WriteString(fmt.Sprintf("pppoptfile = %s\n", optFile)) sb.WriteString("length bit = yes\n") sb.WriteString("redial = yes\n\n") // 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 profObj != nil { switch strings.ToLower(profObj.UseEncryption) { case "no": optSb.WriteString("nomppe\n") case "yes": optSb.WriteString("mppe-128\n") default: optSb.WriteString("require-mppe-128\n") } if profObj.DNSServers != "" { for _, dns := range strings.Split(profObj.DNSServers, ",") { dns = strings.TrimSpace(dns) if dns != "" { optSb.WriteString(fmt.Sprintf("ms-dns %s\n", dns)) } } } if profObj.LocalAddress != "" && profObj.RemoteAddress != "" { local := strings.TrimSpace(profObj.LocalAddress) remote := strings.TrimSpace(profObj.RemoteAddress) if strings.Contains(remote, "-") { remote = strings.TrimSpace(strings.Split(remote, "-")[0]) } if local != "" && remote != "" { optSb.WriteString(fmt.Sprintf("%s:%s\n", local, remote)) } } } else { 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") optSb.WriteString("mru 1410\n") if inst.DefaultRoute { optSb.WriteString("defaultroute\n") optSb.WriteString("replacedefaultroute\n") } _ = os.WriteFile(optFile, []byte(optSb.String()), 0600) } if err := os.WriteFile(Xl2tpdConfigPath, []byte(sb.String()), 0600); err != nil { return err } if err := exec.Command("systemctl", "restart", "xl2tpd").Run(); err != nil { return err } return nil } func writeL2TPControl(command, name string) error { f, err := os.OpenFile(L2TPControlFIFO, os.O_WRONLY|syscall.O_NONBLOCK, 0600) if err != nil { return fmt.Errorf("L2TP control unavailable: %w", err) } defer f.Close() _, err = fmt.Fprintf(f, "%s %s\n", command, name) return err } func (m *L2TPManager) connectInstance(name string) error { return writeL2TPControl("c", name) } func (m *L2TPManager) disconnectInstance(name string) error { return writeL2TPControl("d", name) } func (m *L2TPManager) refreshRuntimeStatus() { psOut, _ := exec.Command("ps", "-ef").Output() psStr := string(psOut) links, _ := netlink.LinkList() for i := range m.instances { name := m.instances[i].Name if strings.Contains(psStr, name) || strings.Contains(psStr, "xl2tpd") { m.instances[i].Running = true } else { m.instances[i].Running = false } m.instances[i].AssignedIP = "" m.instances[i].PPPDevice = "" for _, l := range links { if strings.HasPrefix(l.Attrs().Name, "ppp") && l.Attrs().Flags&net.FlagUp != 0 { addrs, _ := netlink.AddrList(l, netlink.FAMILY_V4) if len(addrs) > 0 { m.instances[i].AssignedIP = addrs[0].IPNet.String() m.instances[i].PPPDevice = l.Attrs().Name m.instances[i].Running = true break } } } } }