package libbox import ( "encoding/binary" "io" "net" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/outbound" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/service" ) type OutboundGroup struct { Tag string Type string Selectable bool Selected string IsExpand bool items []*OutboundGroupItem } func (g *OutboundGroup) GetItems() OutboundGroupItemIterator { return newIterator(g.items) } type OutboundGroupIterator interface { Next() *OutboundGroup HasNext() bool } type OutboundGroupItem struct { Tag string Type string URLTestTime int64 URLTestDelay int32 } type OutboundGroupItemIterator interface { Next() *OutboundGroupItem HasNext() bool } func (c *CommandClient) handleGroupConn(conn net.Conn) { defer conn.Close() for { groups, err := readGroups(conn) if err != nil { c.handler.Disconnected(err.Error()) return } c.handler.WriteGroups(groups) } } func (s *CommandServer) handleGroupConn(conn net.Conn) error { var interval int64 err := binary.Read(conn, binary.BigEndian, &interval) if err != nil { return E.Cause(err, "read interval") } ticker := time.NewTicker(time.Duration(interval)) defer ticker.Stop() ctx := connKeepAlive(conn) for { service := s.service if service != nil { err := writeGroups(conn, service) if err != nil { return err } } else { err := binary.Write(conn, binary.BigEndian, uint16(0)) if err != nil { return err } } select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: } select { case <-ctx.Done(): return ctx.Err() case <-s.urlTestUpdate: } } } func readGroups(reader io.Reader) (OutboundGroupIterator, error) { var groupLength uint16 err := binary.Read(reader, binary.BigEndian, &groupLength) if err != nil { return nil, err } groups := make([]*OutboundGroup, 0, groupLength) for i := 0; i < int(groupLength); i++ { var group OutboundGroup group.Tag, err = rw.ReadVString(reader) if err != nil { return nil, err } group.Type, err = rw.ReadVString(reader) if err != nil { return nil, err } err = binary.Read(reader, binary.BigEndian, &group.Selectable) if err != nil { return nil, err } group.Selected, err = rw.ReadVString(reader) if err != nil { return nil, err } err = binary.Read(reader, binary.BigEndian, &group.IsExpand) if err != nil { return nil, err } var itemLength uint16 err = binary.Read(reader, binary.BigEndian, &itemLength) if err != nil { return nil, err } group.items = make([]*OutboundGroupItem, itemLength) for j := 0; j < int(itemLength); j++ { var item OutboundGroupItem item.Tag, err = rw.ReadVString(reader) if err != nil { return nil, err } item.Type, err = rw.ReadVString(reader) if err != nil { return nil, err } err = binary.Read(reader, binary.BigEndian, &item.URLTestTime) if err != nil { return nil, err } err = binary.Read(reader, binary.BigEndian, &item.URLTestDelay) if err != nil { return nil, err } group.items[j] = &item } groups = append(groups, &group) } return newIterator(groups), nil } func writeGroups(writer io.Writer, boxService *BoxService) error { historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) outbounds := boxService.instance.Router().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { if group, isGroup := it.(adapter.OutboundGroup); isGroup { iGroups = append(iGroups, group) } } var groups []OutboundGroup for _, iGroup := range iGroups { var group OutboundGroup group.Tag = iGroup.Tag() group.Type = iGroup.Type() _, group.Selectable = iGroup.(*outbound.Selector) group.Selected = iGroup.Now() if cacheFile != nil { if isExpand, loaded := cacheFile.LoadGroupExpand(group.Tag); loaded { group.IsExpand = isExpand } } for _, itemTag := range iGroup.All() { itemOutbound, isLoaded := boxService.instance.Router().Outbound(itemTag) if !isLoaded { continue } var item OutboundGroupItem item.Tag = itemTag item.Type = itemOutbound.Type() if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(itemOutbound)); history != nil { item.URLTestTime = history.Time.Unix() item.URLTestDelay = int32(history.Delay) } group.items = append(group.items, &item) } if len(group.items) < 2 { continue } groups = append(groups, group) } err := binary.Write(writer, binary.BigEndian, uint16(len(groups))) if err != nil { return err } for _, group := range groups { err = rw.WriteVString(writer, group.Tag) if err != nil { return err } err = rw.WriteVString(writer, group.Type) if err != nil { return err } err = binary.Write(writer, binary.BigEndian, group.Selectable) if err != nil { return err } err = rw.WriteVString(writer, group.Selected) if err != nil { return err } err = binary.Write(writer, binary.BigEndian, group.IsExpand) if err != nil { return err } err = binary.Write(writer, binary.BigEndian, uint16(len(group.items))) if err != nil { return err } for _, item := range group.items { err = rw.WriteVString(writer, item.Tag) if err != nil { return err } err = rw.WriteVString(writer, item.Type) if err != nil { return err } err = binary.Write(writer, binary.BigEndian, item.URLTestTime) if err != nil { return err } err = binary.Write(writer, binary.BigEndian, item.URLTestDelay) if err != nil { return err } } } return nil } func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { conn, err := c.directConnect() if err != nil { return err } defer conn.Close() err = binary.Write(conn, binary.BigEndian, uint8(CommandGroupExpand)) if err != nil { return err } err = rw.WriteVString(conn, groupTag) if err != nil { return err } err = binary.Write(conn, binary.BigEndian, isExpand) if err != nil { return err } return readError(conn) } func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { groupTag, err := rw.ReadVString(conn) if err != nil { return err } var isExpand bool err = binary.Read(conn, binary.BigEndian, &isExpand) if err != nil { return err } serviceNow := s.service if serviceNow == nil { return writeError(conn, E.New("service not ready")) } cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx) if cacheFile != nil { err = cacheFile.StoreGroupExpand(groupTag, isExpand) if err != nil { return writeError(conn, err) } } return writeError(conn, nil) }