mirror of
				https://github.com/luscis/openlan.git
				synced 2025-10-30 20:16:22 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			593 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			593 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
		
			Executable File
		
	
	
	
	
| package cswitch
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"os/exec"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/luscis/openlan/pkg/api"
 | |
| 	"github.com/luscis/openlan/pkg/app"
 | |
| 	"github.com/luscis/openlan/pkg/cache"
 | |
| 	co "github.com/luscis/openlan/pkg/config"
 | |
| 	"github.com/luscis/openlan/pkg/libol"
 | |
| 	"github.com/luscis/openlan/pkg/models"
 | |
| 	"github.com/luscis/openlan/pkg/network"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	UDPBin = "openudp"
 | |
| )
 | |
| 
 | |
| func GetSocketServer(s *co.Switch) libol.SocketServer {
 | |
| 	crypt := s.Crypt
 | |
| 	block := libol.NewBlockCrypt(crypt.Algo, crypt.Secret)
 | |
| 	switch s.Protocol {
 | |
| 	case "kcp":
 | |
| 		c := libol.NewKcpConfig()
 | |
| 		c.Block = block
 | |
| 		c.Timeout = time.Duration(s.Timeout) * time.Second
 | |
| 		return libol.NewKcpServer(s.Listen, c)
 | |
| 	case "tcp":
 | |
| 		c := &libol.TcpConfig{
 | |
| 			Block:   block,
 | |
| 			Timeout: time.Duration(s.Timeout) * time.Second,
 | |
| 			RdQus:   s.Queue.SockRd,
 | |
| 			WrQus:   s.Queue.SockWr,
 | |
| 		}
 | |
| 		return libol.NewTcpServer(s.Listen, c)
 | |
| 	case "udp":
 | |
| 		c := &libol.UdpConfig{
 | |
| 			Block:   block,
 | |
| 			Timeout: time.Duration(s.Timeout) * time.Second,
 | |
| 		}
 | |
| 		return libol.NewUdpServer(s.Listen, c)
 | |
| 	case "ws":
 | |
| 		c := &libol.WebConfig{
 | |
| 			Block:   block,
 | |
| 			Timeout: time.Duration(s.Timeout) * time.Second,
 | |
| 			RdQus:   s.Queue.SockRd,
 | |
| 			WrQus:   s.Queue.SockWr,
 | |
| 		}
 | |
| 		return libol.NewWebServer(s.Listen, c)
 | |
| 	case "wss":
 | |
| 		c := &libol.WebConfig{
 | |
| 			Block:   block,
 | |
| 			Timeout: time.Duration(s.Timeout) * time.Second,
 | |
| 			RdQus:   s.Queue.SockRd,
 | |
| 			WrQus:   s.Queue.SockWr,
 | |
| 		}
 | |
| 		if s.Cert != nil {
 | |
| 			c.Cert = &libol.CertConfig{
 | |
| 				Crt: s.Cert.CrtFile,
 | |
| 				Key: s.Cert.KeyFile,
 | |
| 			}
 | |
| 		}
 | |
| 		return libol.NewWebServer(s.Listen, c)
 | |
| 	default:
 | |
| 		c := &libol.TcpConfig{
 | |
| 			Block:   block,
 | |
| 			Timeout: time.Duration(s.Timeout) * time.Second,
 | |
| 			RdQus:   s.Queue.SockRd,
 | |
| 			WrQus:   s.Queue.SockWr,
 | |
| 		}
 | |
| 		if s.Cert != nil {
 | |
| 			c.Tls = s.Cert.GetTlsCfg()
 | |
| 		}
 | |
| 		return libol.NewTcpServer(s.Listen, c)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type Apps struct {
 | |
| 	Auth     *app.Access
 | |
| 	Request  *app.Request
 | |
| 	Neighbor *app.Neighbors
 | |
| 	OnLines  *app.Online
 | |
| }
 | |
| 
 | |
| type Hook func(client libol.SocketClient, frame *libol.FrameMessage) error
 | |
| 
 | |
| type Switch struct {
 | |
| 	lock    sync.Mutex
 | |
| 	cfg     *co.Switch
 | |
| 	apps    Apps
 | |
| 	fire    *network.FireWallGlobal
 | |
| 	hooks   []Hook
 | |
| 	http    *Http
 | |
| 	server  libol.SocketServer
 | |
| 	worker  map[string]api.Networker
 | |
| 	uuid    string
 | |
| 	newTime int64
 | |
| 	out     *libol.SubLogger
 | |
| 	confd   *ConfD
 | |
| 	l2tp    *L2TP
 | |
| }
 | |
| 
 | |
| func NewSwitch(c *co.Switch) *Switch {
 | |
| 	server := GetSocketServer(c)
 | |
| 	v := &Switch{
 | |
| 		cfg:     c,
 | |
| 		fire:    network.NewFireWallGlobal(c.FireWall),
 | |
| 		worker:  make(map[string]api.Networker, 32),
 | |
| 		server:  server,
 | |
| 		newTime: time.Now().Unix(),
 | |
| 		hooks:   make([]Hook, 0, 64),
 | |
| 		out:     libol.NewSubLogger(c.Alias),
 | |
| 	}
 | |
| 	v.confd = NewConfd(v)
 | |
| 	return v
 | |
| }
 | |
| 
 | |
| func (v *Switch) Protocol() string {
 | |
| 	if v.cfg == nil {
 | |
| 		return ""
 | |
| 	}
 | |
| 	return v.cfg.Protocol
 | |
| }
 | |
| 
 | |
| func (v *Switch) enablePort(protocol, port string) {
 | |
| 	v.out.Info("Switch.enablePort %s %s", protocol, port)
 | |
| 	// allowed forward between source and prefix.
 | |
| 	v.fire.AddRule(network.IPRule{
 | |
| 		Table:   network.TFilter,
 | |
| 		Chain:   network.OLCInput,
 | |
| 		Proto:   protocol,
 | |
| 		Match:   "multiport",
 | |
| 		DstPort: port,
 | |
| 		Comment: "Open Default Ports",
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (v *Switch) preNetwork() {
 | |
| 	for _, nCfg := range v.cfg.Network {
 | |
| 		name := nCfg.Name
 | |
| 		w := NewNetworker(nCfg)
 | |
| 		v.worker[name] = w
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *Switch) preApplication() {
 | |
| 	// Append accessed auth for point
 | |
| 	v.apps.Auth = app.NewAccess(v)
 | |
| 	v.hooks = append(v.hooks, v.apps.Auth.OnFrame)
 | |
| 	// Append request process
 | |
| 	v.apps.Request = app.NewRequest(v)
 | |
| 	v.hooks = append(v.hooks, v.apps.Request.OnFrame)
 | |
| 
 | |
| 	v.apps.Neighbor = app.NewNeighbors(v)
 | |
| 	v.hooks = append(v.hooks, v.apps.Neighbor.OnFrame)
 | |
| 
 | |
| 	v.apps.OnLines = app.NewOnline(v)
 | |
| 	v.hooks = append(v.hooks, v.apps.OnLines.OnFrame)
 | |
| 
 | |
| 	for i, h := range v.hooks {
 | |
| 		v.out.Debug("Switch.preApplication: id %d, func %s", i, libol.FunName(h))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *Switch) GetPort(listen string) string {
 | |
| 	_, port := libol.GetHostPort(listen)
 | |
| 	return port
 | |
| }
 | |
| 
 | |
| func (v *Switch) openPorts() {
 | |
| 	port := v.GetPort(v.cfg.Listen)
 | |
| 	UdpPorts := []string{"4500", "4500", "8472", "4789", port}
 | |
| 	TcpPorts := []string{"7471", port}
 | |
| 	if v.cfg.Http != nil {
 | |
| 		TcpPorts = append(TcpPorts, v.GetPort(v.cfg.Http.Listen))
 | |
| 	}
 | |
| 	v.enablePort("udp", strings.Join(UdpPorts, ","))
 | |
| 	v.enablePort("tcp", strings.Join(TcpPorts, ","))
 | |
| }
 | |
| 
 | |
| func (v *Switch) SetPass(file string) {
 | |
| 	cache.User.SetFile(file)
 | |
| }
 | |
| 
 | |
| func (v *Switch) LoadPass() {
 | |
| 	cache.User.Load()
 | |
| }
 | |
| 
 | |
| func (v *Switch) LoadLDAP() {
 | |
| 	ldap := v.cfg.Ldap
 | |
| 	if ldap == nil {
 | |
| 		return
 | |
| 	}
 | |
| 	ldapCfg := libol.LDAPConfig{
 | |
| 		Server:    ldap.Server,
 | |
| 		BindUser:  ldap.BindDN,
 | |
| 		BindPass:  ldap.BindPass,
 | |
| 		BaseDN:    ldap.BaseDN,
 | |
| 		Attr:      ldap.Attribute,
 | |
| 		Filter:    ldap.Filter,
 | |
| 		EnableTls: ldap.Tls,
 | |
| 	}
 | |
| 	promise := &libol.Promise{
 | |
| 		First:  time.Second * 2,
 | |
| 		MaxInt: time.Minute,
 | |
| 		MinInt: time.Second * 10,
 | |
| 	}
 | |
| 	promise.Go(func() error {
 | |
| 		return cache.User.SetLdap(&ldapCfg)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (v *Switch) Initialize() {
 | |
| 	v.lock.Lock()
 | |
| 	defer v.lock.Unlock()
 | |
| 
 | |
| 	v.openPorts()
 | |
| 	v.preApplication()
 | |
| 	if v.cfg.Http != nil {
 | |
| 		v.http = NewHttp(v)
 | |
| 	}
 | |
| 	v.preNetwork()
 | |
| 	// Load global firewall
 | |
| 	v.fire.Initialize()
 | |
| 	for _, w := range v.worker {
 | |
| 		if w.Provider() == "vxlan" {
 | |
| 			continue
 | |
| 		}
 | |
| 		w.Initialize()
 | |
| 	}
 | |
| 	// Load password for guest access
 | |
| 	v.SetPass(v.cfg.PassFile)
 | |
| 	v.LoadPass()
 | |
| 
 | |
| 	v.LoadLDAP()
 | |
| 
 | |
| 	// Enable cert verify for access
 | |
| 	cert := v.cfg.Cert
 | |
| 	if cert != nil {
 | |
| 		cache.User.SetCert(&libol.CertConfig{
 | |
| 			Crt: cert.CrtFile,
 | |
| 		})
 | |
| 	}
 | |
| 	// Enable L2TP
 | |
| 	if v.cfg.L2TP != nil {
 | |
| 		v.l2tp = NewL2TP(v.cfg.L2TP)
 | |
| 	}
 | |
| 	// Start confd monitor
 | |
| 	v.confd.Initialize()
 | |
| }
 | |
| 
 | |
| func (v *Switch) onFrame(client libol.SocketClient, frame *libol.FrameMessage) error {
 | |
| 	for _, h := range v.hooks {
 | |
| 		if v.out.Has(libol.LOG) {
 | |
| 			v.out.Log("Switch.onFrame: %s", libol.FunName(h))
 | |
| 		}
 | |
| 		if h != nil {
 | |
| 			if err := h(client, frame); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (v *Switch) OnClient(client libol.SocketClient) error {
 | |
| 	client.SetStatus(libol.ClConnected)
 | |
| 	v.out.Info("Switch.onClient: %s", client.String())
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (v *Switch) SignIn(client libol.SocketClient) error {
 | |
| 	v.out.Cmd("Switch.SignIn %s", client.String())
 | |
| 	data := struct {
 | |
| 		Address string `json:"address"`
 | |
| 		Switch  string `json:"switch"`
 | |
| 	}{
 | |
| 		Address: client.String(),
 | |
| 		Switch:  client.LocalAddr(),
 | |
| 	}
 | |
| 	body, err := json.Marshal(data)
 | |
| 	if err != nil {
 | |
| 		v.out.Error("Switch.SignIn: %s", err)
 | |
| 		return err
 | |
| 	}
 | |
| 	v.out.Cmd("Switch.SignIn: %s", body)
 | |
| 	m := libol.NewControlFrame(libol.SignReq, body)
 | |
| 	if err := client.WriteMsg(m); err != nil {
 | |
| 		v.out.Error("Switch.SignIn: %s", err)
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func client2Point(client libol.SocketClient) (*models.Point, error) {
 | |
| 	addr := client.RemoteAddr()
 | |
| 	if private := client.Private(); private == nil {
 | |
| 		return nil, libol.NewErr("point %s notFound.", addr)
 | |
| 	} else {
 | |
| 		obj, ok := private.(*models.Point)
 | |
| 		if !ok {
 | |
| 			return nil, libol.NewErr("point %s notRight.", addr)
 | |
| 		}
 | |
| 		return obj, nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *Switch) ReadClient(client libol.SocketClient, frame *libol.FrameMessage) error {
 | |
| 	addr := client.RemoteAddr()
 | |
| 	if v.out.Has(libol.LOG) {
 | |
| 		v.out.Log("Switch.ReadClient: %s %x", addr, frame.Frame())
 | |
| 	}
 | |
| 	frame.Decode()
 | |
| 	if err := v.onFrame(client, frame); err != nil {
 | |
| 		v.out.Debug("Switch.ReadClient: %s dropping by %s", addr, err)
 | |
| 		if frame.Action() == libol.PingReq {
 | |
| 			// send sign message to point require login.
 | |
| 			_ = v.SignIn(client)
 | |
| 		}
 | |
| 		return nil
 | |
| 	}
 | |
| 	if frame.IsControl() {
 | |
| 		return nil
 | |
| 	}
 | |
| 	// process ethernet frame message.
 | |
| 	obj, err := client2Point(client)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	device := obj.Device
 | |
| 	if device == nil {
 | |
| 		return libol.NewErr("Tap devices is nil")
 | |
| 	}
 | |
| 	if _, err := device.Write(frame.Frame()); err != nil {
 | |
| 		v.out.Error("Switch.ReadClient: %s", err)
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (v *Switch) OnClose(client libol.SocketClient) error {
 | |
| 	addr := client.RemoteAddr()
 | |
| 	v.out.Info("Switch.OnClose: %s", addr)
 | |
| 	if obj, err := client2Point(client); err == nil {
 | |
| 		cache.Network.DelLease(obj.Alias, obj.Network)
 | |
| 	}
 | |
| 	cache.Point.Del(addr)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (v *Switch) openUdp() {
 | |
| 	args := []string{
 | |
| 		"-port", strconv.Itoa(co.EspLocalUdp),
 | |
| 		"-log:file", "/var/openlan/openudp.log",
 | |
| 	}
 | |
| 	libol.Info("%s %v", UDPBin, args)
 | |
| 	cmd := exec.Command(UDPBin, args...)
 | |
| 	if err := cmd.Run(); err != nil {
 | |
| 		libol.Error("Switch.OpenUdp %s", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *Switch) Start() {
 | |
| 	v.lock.Lock()
 | |
| 	defer v.lock.Unlock()
 | |
| 
 | |
| 	v.fire.Start()
 | |
| 	// firstly, start network.
 | |
| 	for _, w := range v.worker {
 | |
| 		if w.Provider() == "vxlan" {
 | |
| 			continue
 | |
| 		}
 | |
| 		w.Start(v)
 | |
| 	}
 | |
| 	// start server for accessing
 | |
| 	libol.Go(v.server.Accept)
 | |
| 	call := libol.ServerListener{
 | |
| 		OnClient: v.OnClient,
 | |
| 		OnClose:  v.OnClose,
 | |
| 		ReadAt:   v.ReadClient,
 | |
| 	}
 | |
| 	libol.Go(func() { v.server.Loop(call) })
 | |
| 	if v.http != nil {
 | |
| 		libol.Go(v.http.Start)
 | |
| 	}
 | |
| 	libol.Go(v.confd.Start)
 | |
| 	if v.l2tp != nil {
 | |
| 		libol.Go(v.l2tp.Start)
 | |
| 	}
 | |
| 	libol.Go(v.openUdp)
 | |
| }
 | |
| 
 | |
| func (v *Switch) Stop() {
 | |
| 	v.lock.Lock()
 | |
| 	defer v.lock.Unlock()
 | |
| 
 | |
| 	v.out.Info("Switch.Stop")
 | |
| 
 | |
| 	if v.l2tp != nil {
 | |
| 		v.l2tp.Stop()
 | |
| 	}
 | |
| 	v.confd.Stop()
 | |
| 	if v.http != nil {
 | |
| 		v.http.Shutdown()
 | |
| 	}
 | |
| 	// stop network.
 | |
| 	for _, w := range v.worker {
 | |
| 		if w.Provider() == "vxlan" {
 | |
| 			continue
 | |
| 		}
 | |
| 		w.Stop()
 | |
| 	}
 | |
| 	v.out.Info("Switch.Stop left points")
 | |
| 	// notify leave to point.
 | |
| 	for p := range cache.Point.List() {
 | |
| 		if p == nil {
 | |
| 			break
 | |
| 		}
 | |
| 		v.leftClient(p.Client)
 | |
| 	}
 | |
| 	v.server.Close()
 | |
| 	v.fire.Stop()
 | |
| }
 | |
| 
 | |
| func (v *Switch) Alias() string {
 | |
| 	return v.cfg.Alias
 | |
| }
 | |
| 
 | |
| func (v *Switch) UpTime() int64 {
 | |
| 	return time.Now().Unix() - v.newTime
 | |
| }
 | |
| 
 | |
| func (v *Switch) Server() libol.SocketServer {
 | |
| 	return v.server
 | |
| }
 | |
| 
 | |
| func (v *Switch) GetBridge(tenant string) (network.Bridger, error) {
 | |
| 	w, ok := v.worker[tenant]
 | |
| 	if !ok {
 | |
| 		return nil, libol.NewErr("bridge %s notFound", tenant)
 | |
| 	}
 | |
| 	return w.Bridge(), nil
 | |
| }
 | |
| 
 | |
| func (v *Switch) NewTap(tenant string) (network.Taper, error) {
 | |
| 	v.lock.Lock()
 | |
| 	defer v.lock.Unlock()
 | |
| 	v.out.Debug("Switch.NewTap")
 | |
| 
 | |
| 	// already not need support free list for device.
 | |
| 	// dropped firstly packages during 15s because of forwarding delay.
 | |
| 	br, err := v.GetBridge(tenant)
 | |
| 	if err != nil {
 | |
| 		v.out.Error("Switch.NewTap: %s", err)
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	dev, err := network.NewTaper(tenant, network.TapConfig{
 | |
| 		Provider: br.Type(),
 | |
| 		Type:     network.TAP,
 | |
| 		VirBuf:   v.cfg.Queue.VirWrt,
 | |
| 		KernBuf:  v.cfg.Queue.VirSnd,
 | |
| 		Name:     "auto",
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		v.out.Error("Switch.NewTap: %s", err)
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	dev.Up()
 | |
| 	// add new tap to bridge.
 | |
| 	_ = br.AddSlave(dev.Name())
 | |
| 	v.out.Info("Switch.NewTap: %s on %s", dev.Name(), tenant)
 | |
| 	return dev, nil
 | |
| }
 | |
| 
 | |
| func (v *Switch) FreeTap(dev network.Taper) error {
 | |
| 	v.lock.Lock()
 | |
| 	defer v.lock.Unlock()
 | |
| 	name := dev.Name()
 | |
| 	tenant := dev.Tenant()
 | |
| 	v.out.Debug("Switch.FreeTap %s", name)
 | |
| 	w, ok := v.worker[tenant]
 | |
| 	if !ok {
 | |
| 		return libol.NewErr("bridge %s notFound", tenant)
 | |
| 	}
 | |
| 	br := w.Bridge()
 | |
| 	_ = br.DelSlave(dev.Name())
 | |
| 	v.out.Info("Switch.FreeTap: %s", name)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (v *Switch) UUID() string {
 | |
| 	if v.uuid == "" {
 | |
| 		v.uuid = libol.GenString(13)
 | |
| 	}
 | |
| 	return v.uuid
 | |
| }
 | |
| 
 | |
| func (v *Switch) ReadTap(device network.Taper, readAt func(f *libol.FrameMessage) error) {
 | |
| 	name := device.Name()
 | |
| 	v.out.Info("Switch.ReadTap: %s", name)
 | |
| 	done := make(chan bool, 2)
 | |
| 	queue := make(chan *libol.FrameMessage, v.cfg.Queue.TapWr)
 | |
| 	libol.Go(func() {
 | |
| 		for {
 | |
| 			frame := libol.NewFrameMessage(0)
 | |
| 			n, err := device.Read(frame.Frame())
 | |
| 			if err != nil {
 | |
| 				v.out.Error("Switch.ReadTap: %s", err)
 | |
| 				done <- true
 | |
| 				break
 | |
| 			}
 | |
| 			frame.SetSize(n)
 | |
| 			if v.out.Has(libol.LOG) {
 | |
| 				v.out.Log("Switch.ReadTap: %x\n", frame.Frame()[:n])
 | |
| 			}
 | |
| 			queue <- frame
 | |
| 		}
 | |
| 	})
 | |
| 	defer device.Close()
 | |
| 	for {
 | |
| 		select {
 | |
| 		case frame := <-queue:
 | |
| 			if err := readAt(frame); err != nil {
 | |
| 				v.out.Error("Switch.ReadTap: readAt %s %s", name, err)
 | |
| 				return
 | |
| 			}
 | |
| 		case <-done:
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *Switch) OffClient(client libol.SocketClient) {
 | |
| 	v.out.Info("Switch.OffClient: %s", client)
 | |
| 	if v.server != nil {
 | |
| 		v.server.OffClient(client)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *Switch) Config() *co.Switch {
 | |
| 	return v.cfg
 | |
| }
 | |
| 
 | |
| func (v *Switch) leftClient(client libol.SocketClient) {
 | |
| 	if client == nil {
 | |
| 		return
 | |
| 	}
 | |
| 	v.out.Info("Switch.leftClient: %s", client.String())
 | |
| 	data := struct {
 | |
| 		DateTime   int64  `json:"datetime"`
 | |
| 		UUID       string `json:"uuid"`
 | |
| 		Alias      string `json:"alias"`
 | |
| 		Connection string `json:"connection"`
 | |
| 		Address    string `json:"address"`
 | |
| 	}{
 | |
| 		DateTime:   time.Now().Unix(),
 | |
| 		UUID:       v.UUID(),
 | |
| 		Alias:      v.Alias(),
 | |
| 		Address:    client.LocalAddr(),
 | |
| 		Connection: client.RemoteAddr(),
 | |
| 	}
 | |
| 	body, err := json.Marshal(data)
 | |
| 	if err != nil {
 | |
| 		v.out.Error("Switch.leftClient: %s", err)
 | |
| 		return
 | |
| 	}
 | |
| 	v.out.Cmd("Switch.leftClient: %s", body)
 | |
| 	m := libol.NewControlFrame(libol.LeftReq, body)
 | |
| 	if err := client.WriteMsg(m); err != nil {
 | |
| 		v.out.Error("Switch.leftClient: %s", err)
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *Switch) Reload() {
 | |
| 	co.Reload()
 | |
| 	cache.Reload()
 | |
| 
 | |
| 	v.fire.Start()
 | |
| 	for _, w := range v.worker {
 | |
| 		w.Reload(v)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *Switch) Save() {
 | |
| 	v.cfg.Save()
 | |
| }
 | 
