package vpn import ( "encoding/json" "fmt" "net" "os" "os/exec" "path/filepath" "strings" "sync" "time" "github.com/vishvananda/netlink" ) const ( L2TPInstancesStorePath = "/etc/proxmox-router/vpn/l2tp_instances.json" Xl2tpdConfigPath = "/etc/xl2tpd/xl2tpd.conf" L2TPControlFIFO = "/var/run/xl2tpd/l2tp-control" ) type L2TPManager struct { mu sync.Mutex instances []L2TPInstance } func NewL2TPManager() *L2TPManager { m := &L2TPManager{ instances: make([]L2TPInstance, 0), } m.load() return m } 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") } 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 { go m.connectInstance(inst.Name) } else { go m.disconnectInstance(inst.Name) } return nil } 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 _ = m.save() 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 _ = m.save() if enabled { go m.connectInstance(name) } else { go m.disconnectInstance(name) } return nil } } 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 { 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("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") 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 } _ = exec.Command("systemctl", "restart", "xl2tpd").Run() return nil } func (m *L2TPManager) connectInstance(name string) error { time.Sleep(300 * time.Millisecond) if _, err := os.Stat(L2TPControlFIFO); err == nil { f, err := os.OpenFile(L2TPControlFIFO, os.O_WRONLY, 0600) if err == nil { _, _ = f.WriteString(fmt.Sprintf("c %s\n", name)) _ = f.Close() } } return nil } func (m *L2TPManager) disconnectInstance(name string) error { if _, err := os.Stat(L2TPControlFIFO); err == nil { f, err := os.OpenFile(L2TPControlFIFO, os.O_WRONLY, 0600) if err == nil { _, _ = f.WriteString(fmt.Sprintf("d %s\n", name)) _ = f.Close() } } return nil } 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 } } } } }