package _switch import ( "bytes" "fmt" "io/ioutil" "os" "os/exec" "path" "path/filepath" "strings" "text/template" co "github.com/luscis/openlan/pkg/config" "github.com/luscis/openlan/pkg/libol" ) const ( OpenVPNBin = "openvpn" DefaultCurDir = "/var/openlan/openvpn/default" ) type OpenVPNData struct { Local string Port string CertNot bool Ca string Cert string Key string DhPem string TlsAuth string Cipher string Server string Device string Protocol string Script string Routes []string Renego int Stats string IpIp string Push []string ClientConfigDir string } const ( xAuthConfTmpl = `# Generate by OpenLAN local {{ .Local }} port {{ .Port }} proto {{ .Protocol }} dev {{ .Device }} reneg-sec {{ .Renego }} keepalive 10 120 persist-key persist-tun ca {{ .Ca }} cert {{ .Cert }} key {{ .Key }} dh {{ .DhPem }} server {{ .Server }} {{- range .Routes }} push "route {{ . }}" {{- end }} {{- range .Push }} push "{{ . }}" {{- end }} ifconfig-pool-persist {{ .Protocol }}{{ .Port }}ipp tls-auth {{ .TlsAuth }} 0 cipher {{ .Cipher }} status {{ .Protocol }}{{ .Port }}server.status 5 {{- if .CertNot }} client-cert-not-required {{- else }} verify-client-cert none {{- end }} script-security 3 auth-user-pass-verify "{{ .Script }}" via-env username-as-common-name client-config-dir {{ .ClientConfigDir }} verb 3 ` certConfTmpl = `# Generate by OpenLAN local {{ .Local }} port {{ .Port }} proto {{ .Protocol }} dev {{ .Device }} reneg-sec {{ .Renego }} keepalive 10 120 persist-key persist-tun ca {{ .Ca }} cert {{ .Cert }} key {{ .Key }} dh {{ .DhPem }} server {{ .Server }} {{- range .Routes }} push "route {{ . }}" {{- end }} ifconfig-pool-persist {{ .Protocol }}{{ .Port }}ipp tls-auth {{ .TlsAuth }} 0 cipher {{ .Cipher }} status {{ .Protocol }}{{ .Port }}server.status 5 client-config-dir {{ .ClientConfigDir }} verb 3 ` ) func NewOpenVpnDataFromConf(obj *OpenVPN) *OpenVPNData { cfg := obj.Cfg data := &OpenVPNData{ Local: obj.Local, Port: obj.Port, CertNot: true, Ca: cfg.RootCa, Cert: cfg.ServerCrt, Key: cfg.ServerKey, DhPem: cfg.DhPem, TlsAuth: cfg.TlsAuth, Cipher: cfg.Cipher, Device: cfg.Device, Protocol: cfg.Protocol, Script: cfg.Script, Renego: cfg.Renego, Push: cfg.Push, } if cfg.Version > 23 { data.CertNot = false } addr, _ := libol.IPNetwork(cfg.Subnet) data.Server = strings.ReplaceAll(addr, "/", " ") for _, rt := range cfg.Routes { if addr, err := libol.IPNetwork(rt); err == nil { r := strings.ReplaceAll(addr, "/", " ") data.Routes = append(data.Routes, r) } } data.ClientConfigDir = obj.DirectoryClientConfig() return data } type OpenVPN struct { Cfg *co.OpenVPN out *libol.SubLogger Protocol string Local string Port string } func NewOpenVPN(cfg *co.OpenVPN) *OpenVPN { obj := &OpenVPN{ Cfg: cfg, out: libol.NewSubLogger(cfg.Network), Protocol: cfg.Protocol, Local: "0.0.0.0", Port: "4494", } obj.Local = strings.SplitN(cfg.Listen, ":", 2)[0] if strings.Contains(cfg.Listen, ":") { obj.Port = strings.SplitN(cfg.Listen, ":", 2)[1] } return obj } func (o *OpenVPN) ID() string { return o.Protocol + o.Port } func (o *OpenVPN) Path() string { return OpenVPNBin } func (o *OpenVPN) Directory() string { if o.Cfg == nil { return DefaultCurDir } return o.Cfg.Directory } func (o *OpenVPN) FileCfg(full bool) string { if o.Cfg == nil { return "" } name := o.ID() + "server.conf" if !full { return name } return filepath.Join(o.Cfg.Directory, name) } func (o *OpenVPN) FileClient(full bool) string { if o.Cfg == nil { return "" } name := o.ID() + "client.ovpn" if !full { return name } return filepath.Join(o.Cfg.Directory, name) } func (o *OpenVPN) FileLog(full bool) string { if o.Cfg == nil { return "" } name := o.ID() + "server.log" if !full { return name } return filepath.Join(o.Cfg.Directory, name) } func (o *OpenVPN) FilePid(full bool) string { if o.Cfg == nil { return "" } name := o.ID() + "server.pid" if !full { return name } return filepath.Join(o.Cfg.Directory, name) } func (o *OpenVPN) FileStats(full bool) string { if o.Cfg == nil { return "" } name := o.ID() + "server.stats" if !full { return name } return filepath.Join(o.Cfg.Directory, name) } func (o *OpenVPN) ServerTmpl() string { tmplStr := xAuthConfTmpl if o.Cfg.Auth == "cert" { tmplStr = certConfTmpl } cfgTmpl := filepath.Join(o.Cfg.Directory, o.ID()+"server.tmpl") _ = ioutil.WriteFile(cfgTmpl, []byte(tmplStr), 0600) return tmplStr } func (o *OpenVPN) FileIpp(full bool) string { if o.Cfg == nil { return "" } name := o.ID() + "ipp" if !full { return name } return filepath.Join(o.Cfg.Directory, name) } func (o *OpenVPN) DirectoryClientConfig() string { if o.Cfg == nil { return path.Join(DefaultCurDir, "ccd") } return path.Join(o.Cfg.Directory, "ccd") } func (o *OpenVPN) WriteConf(path string) error { fp, err := libol.CreateFile(path) if err != nil || fp == nil { return err } defer fp.Close() data := NewOpenVpnDataFromConf(o) o.out.Debug("OpenVPN.WriteConf %v", data) if data.ClientConfigDir != "" { _ = o.writeClientConfig() } tmplStr := o.ServerTmpl() if tmpl, err := template.New("main").Parse(tmplStr); err != nil { return err } else { if err := tmpl.Execute(fp, data); err != nil { return err } } return nil } func (o *OpenVPN) writeClientConfig() error { // make client dir and config file ccd := o.DirectoryClientConfig() if err := os.Mkdir(ccd, 0600); err != nil { o.out.Info("OpenVPN.writeClientConfig %s", err) } for _, fic := range o.Cfg.Clients { if fic.Name == "" || fic.Address == "" { continue } ficFile := filepath.Join(ccd, fic.Name) pushIP := fmt.Sprintf("ifconfig-push %s %s", fic.Address, fic.Netmask) if err := ioutil.WriteFile(ficFile, []byte(pushIP), 0600); err != nil { o.out.Warn("OpenVPN.writeClientConfig %s", err) } } return nil } func (o *OpenVPN) Clean() { ccd := o.DirectoryClientConfig() for _, fic := range o.Cfg.Clients { if fic.Name == "" || fic.Address == "" { continue } file := filepath.Join(ccd, fic.Name) if err := libol.FileExist(file); err == nil { if err := os.Remove(file); err != nil { o.out.Warn("OpenVPN.Clean %s", err) } } } files := []string{o.FileStats(true), o.FileIpp(true)} for _, file := range files { if err := libol.FileExist(file); err == nil { if err := os.Remove(file); err != nil { o.out.Warn("OpenVPN.Clean %s", err) } } } } func (o *OpenVPN) Initialize() { if !o.ValidConf() { return } o.Clean() if err := os.Mkdir(o.Directory(), 0600); err != nil { o.out.Info("OpenVPN.Initialize %s", err) } if err := o.WriteConf(o.FileCfg(true)); err != nil { o.out.Warn("OpenVPN.Initialize %s", err) return } if ctx, err := o.Profile(); err == nil { file := o.FileClient(true) if err := ioutil.WriteFile(file, ctx, 0600); err != nil { o.out.Warn("OpenVPN.Initialize %s", err) } } else { o.out.Warn("OpenVPN.Initialize %s", err) } } func (o *OpenVPN) ValidConf() bool { if o.Cfg == nil { return false } if o.Cfg.Listen == "" || o.Cfg.Subnet == "" { return false } return true } func (o *OpenVPN) Start() { if !o.ValidConf() { return } log, err := libol.CreateFile(o.FileLog(true)) if err != nil { o.out.Warn("OpenVPN.Start %s", err) return } libol.Go(func() { defer log.Close() args := []string{ "--cd", o.Directory(), "--config", o.FileCfg(false), "--writepid", o.FilePid(false), } cmd := exec.Command(o.Path(), args...) cmd.Stdout = log cmd.Stderr = log if err := cmd.Run(); err != nil { o.out.Error("OpenVPN.Start %s: %s", o.ID(), err) } }) } func (o *OpenVPN) Stop() { if !o.ValidConf() { return } if data, err := ioutil.ReadFile(o.FilePid(true)); err != nil { o.out.Debug("OpenVPN.Stop %s", err) } else { pid := strings.TrimSpace(string(data)) cmd := exec.Command("/usr/bin/kill", pid) if err := cmd.Run(); err != nil { o.out.Warn("OpenVPN.Stop %s: %s", pid, err) } } o.Clean() } func (o *OpenVPN) ProfileTmpl() string { tmplStr := xAuthClientProfile if o.Cfg.Auth == "cert" { tmplStr = certClientProfile } cfgTmpl := filepath.Join(o.Cfg.Directory, o.ID()+"client.tmpl") _ = ioutil.WriteFile(cfgTmpl, []byte(tmplStr), 0600) return tmplStr } func (o *OpenVPN) Profile() ([]byte, error) { data := NewOpenVpnProfileFromConf(o) tmplStr := o.ProfileTmpl() tmpl, err := template.New("main").Parse(tmplStr) if err != nil { return nil, err } var out bytes.Buffer if err := tmpl.Execute(&out, data); err == nil { return out.Bytes(), nil } else { return nil, err } } type OpenVPNProfile struct { Server string Port string Ca string Cert string Key string TlsAuth string Cipher string Device string Protocol string Renego int } const ( xAuthClientProfile = `# Generate by OpenLAN client dev {{ .Device }} route-metric 300 proto {{ .Protocol }} remote {{ .Server }} {{ .Port }} reneg-sec {{ .Renego }} resolv-retry infinite nobind persist-key persist-tun {{ .Ca -}} remote-cert-tls server {{ .TlsAuth -}} key-direction 1 cipher {{ .Cipher }} auth-nocache verb 4 auth-user-pass ` certClientProfile = `# Generate by OpenLAN client dev {{ .Device }} route-metric 300 proto {{ .Protocol }} remote {{ .Server }} {{ .Port }} reneg-sec {{ .Renego }} resolv-retry infinite nobind persist-key persist-tun {{ .Ca -}} remote-cert-tls server {{ .TlsAuth -}} key-direction 1 cipher {{ .Cipher }} auth-nocache verb 4 ` ) func NewOpenVpnProfileFromConf(obj *OpenVPN) *OpenVPNProfile { cfg := obj.Cfg data := &OpenVPNProfile{ Server: obj.Local, Port: obj.Port, Cipher: cfg.Cipher, Device: cfg.Device[:3], Protocol: cfg.Protocol, Renego: cfg.Renego, } if ctx, err := ioutil.ReadFile(cfg.RootCa); err == nil { data.Ca = string(ctx) } if ctx, err := ioutil.ReadFile(cfg.TlsAuth); err == nil { data.TlsAuth = string(ctx) } return data }