github.com/igoogolx/clash@v1.19.8/config/config.go (about) 1 package config 2 3 import ( 4 "errors" 5 "fmt" 6 "net" 7 "net/url" 8 "os" 9 "strings" 10 11 "github.com/igoogolx/clash/adapter" 12 "github.com/igoogolx/clash/adapter/outbound" 13 "github.com/igoogolx/clash/adapter/outboundgroup" 14 "github.com/igoogolx/clash/adapter/provider" 15 "github.com/igoogolx/clash/component/auth" 16 "github.com/igoogolx/clash/component/fakeip" 17 "github.com/igoogolx/clash/component/trie" 18 C "github.com/igoogolx/clash/constant" 19 providerTypes "github.com/igoogolx/clash/constant/provider" 20 "github.com/igoogolx/clash/dns" 21 "github.com/igoogolx/clash/log" 22 R "github.com/igoogolx/clash/rule" 23 T "github.com/igoogolx/clash/tunnel" 24 25 "github.com/samber/lo" 26 "gopkg.in/yaml.v3" 27 ) 28 29 // General config 30 type General struct { 31 LegacyInbound 32 Controller 33 Authentication []string `json:"authentication"` 34 Mode T.TunnelMode `json:"mode"` 35 LogLevel log.LogLevel `json:"log-level"` 36 IPv6 bool `json:"ipv6"` 37 Interface string `json:"-"` 38 RoutingMark int `json:"-"` 39 } 40 41 // Controller 42 type Controller struct { 43 ExternalController string `json:"-"` 44 ExternalUI string `json:"-"` 45 Secret string `json:"-"` 46 } 47 48 type LegacyInbound struct { 49 Port int `json:"port"` 50 SocksPort int `json:"socks-port"` 51 RedirPort int `json:"redir-port"` 52 TProxyPort int `json:"tproxy-port"` 53 MixedPort int `json:"mixed-port"` 54 AllowLan bool `json:"allow-lan"` 55 BindAddress string `json:"bind-address"` 56 } 57 58 // DNS config 59 type DNS struct { 60 Enable bool `yaml:"enable"` 61 IPv6 bool `yaml:"ipv6"` 62 NameServer []dns.NameServer `yaml:"nameserver"` 63 Fallback []dns.NameServer `yaml:"fallback"` 64 FallbackFilter FallbackFilter `yaml:"fallback-filter"` 65 Listen string `yaml:"listen"` 66 EnhancedMode C.DNSMode `yaml:"enhanced-mode"` 67 DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` 68 FakeIPRange *fakeip.Pool 69 Hosts *trie.DomainTrie 70 NameServerPolicy map[string]dns.NameServer 71 SearchDomains []string 72 } 73 74 // FallbackFilter config 75 type FallbackFilter struct { 76 GeoIP bool `yaml:"geoip"` 77 GeoIPCode string `yaml:"geoip-code"` 78 IPCIDR []*net.IPNet `yaml:"ipcidr"` 79 Domain []string `yaml:"domain"` 80 } 81 82 // Profile config 83 type Profile struct { 84 StoreSelected bool `yaml:"store-selected"` 85 StoreFakeIP bool `yaml:"store-fake-ip"` 86 } 87 88 // Experimental config 89 type Experimental struct { 90 UDPFallbackMatch bool `yaml:"udp-fallback-match"` 91 } 92 93 // Config is clash config manager 94 type Config struct { 95 General *General 96 DNS *DNS 97 Experimental *Experimental 98 Hosts *trie.DomainTrie 99 Profile *Profile 100 Inbounds []C.Inbound 101 Rules []C.Rule 102 Users []auth.AuthUser 103 Proxies map[string]C.Proxy 104 Providers map[string]providerTypes.ProxyProvider 105 Tunnels []Tunnel 106 } 107 108 type RawDNS struct { 109 Enable bool `yaml:"enable"` 110 IPv6 *bool `yaml:"ipv6"` 111 UseHosts bool `yaml:"use-hosts"` 112 NameServer []string `yaml:"nameserver"` 113 Fallback []string `yaml:"fallback"` 114 FallbackFilter RawFallbackFilter `yaml:"fallback-filter"` 115 Listen string `yaml:"listen"` 116 EnhancedMode C.DNSMode `yaml:"enhanced-mode"` 117 FakeIPRange string `yaml:"fake-ip-range"` 118 FakeIPFilter []string `yaml:"fake-ip-filter"` 119 DefaultNameserver []string `yaml:"default-nameserver"` 120 NameServerPolicy map[string]string `yaml:"nameserver-policy"` 121 SearchDomains []string `yaml:"search-domains"` 122 } 123 124 type RawFallbackFilter struct { 125 GeoIP bool `yaml:"geoip"` 126 GeoIPCode string `yaml:"geoip-code"` 127 IPCIDR []string `yaml:"ipcidr"` 128 Domain []string `yaml:"domain"` 129 } 130 131 type tunnel struct { 132 Network []string `yaml:"network"` 133 Address string `yaml:"address"` 134 Target string `yaml:"target"` 135 Proxy string `yaml:"proxy"` 136 } 137 138 type Tunnel tunnel 139 140 // UnmarshalYAML implements yaml.Unmarshaler 141 func (t *Tunnel) UnmarshalYAML(unmarshal func(any) error) error { 142 var tp string 143 if err := unmarshal(&tp); err != nil { 144 var inner tunnel 145 if err := unmarshal(&inner); err != nil { 146 return err 147 } 148 149 *t = Tunnel(inner) 150 return nil 151 } 152 153 // parse udp/tcp,address,target,proxy 154 parts := lo.Map(strings.Split(tp, ","), func(s string, _ int) string { 155 return strings.TrimSpace(s) 156 }) 157 if len(parts) != 4 { 158 return fmt.Errorf("invalid tunnel config %s", tp) 159 } 160 network := strings.Split(parts[0], "/") 161 162 // validate network 163 for _, n := range network { 164 switch n { 165 case "tcp", "udp": 166 default: 167 return fmt.Errorf("invalid tunnel network %s", n) 168 } 169 } 170 171 // validate address and target 172 address := parts[1] 173 target := parts[2] 174 for _, addr := range []string{address, target} { 175 if _, _, err := net.SplitHostPort(addr); err != nil { 176 return fmt.Errorf("invalid tunnel target or address %s", addr) 177 } 178 } 179 180 *t = Tunnel(tunnel{ 181 Network: network, 182 Address: address, 183 Target: target, 184 Proxy: parts[3], 185 }) 186 return nil 187 } 188 189 type RawConfig struct { 190 Port int `yaml:"port"` 191 SocksPort int `yaml:"socks-port"` 192 RedirPort int `yaml:"redir-port"` 193 TProxyPort int `yaml:"tproxy-port"` 194 MixedPort int `yaml:"mixed-port"` 195 Authentication []string `yaml:"authentication"` 196 AllowLan bool `yaml:"allow-lan"` 197 BindAddress string `yaml:"bind-address"` 198 Mode T.TunnelMode `yaml:"mode"` 199 LogLevel log.LogLevel `yaml:"log-level"` 200 IPv6 bool `yaml:"ipv6"` 201 ExternalController string `yaml:"external-controller"` 202 ExternalUI string `yaml:"external-ui"` 203 Secret string `yaml:"secret"` 204 Interface string `yaml:"interface-name"` 205 RoutingMark int `yaml:"routing-mark"` 206 Tunnels []Tunnel `yaml:"tunnels"` 207 208 ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` 209 Hosts map[string]string `yaml:"hosts"` 210 Inbounds []C.Inbound `yaml:"inbounds"` 211 DNS RawDNS `yaml:"dns"` 212 Experimental Experimental `yaml:"experimental"` 213 Profile Profile `yaml:"profile"` 214 Proxy []map[string]any `yaml:"proxies"` 215 ProxyGroup []map[string]any `yaml:"proxy-groups"` 216 Rule []string `yaml:"rules"` 217 } 218 219 // Parse config 220 func Parse(buf []byte) (*Config, error) { 221 rawCfg, err := UnmarshalRawConfig(buf) 222 if err != nil { 223 return nil, err 224 } 225 226 return ParseRawConfig(rawCfg) 227 } 228 229 func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { 230 // config with default value 231 rawCfg := &RawConfig{ 232 AllowLan: false, 233 BindAddress: "*", 234 Mode: T.Rule, 235 Authentication: []string{}, 236 LogLevel: log.INFO, 237 Hosts: map[string]string{}, 238 Rule: []string{}, 239 Proxy: []map[string]any{}, 240 ProxyGroup: []map[string]any{}, 241 DNS: RawDNS{ 242 Enable: false, 243 UseHosts: true, 244 FakeIPRange: "198.18.0.1/16", 245 FallbackFilter: RawFallbackFilter{ 246 GeoIP: true, 247 GeoIPCode: "CN", 248 IPCIDR: []string{}, 249 }, 250 DefaultNameserver: []string{ 251 "114.114.114.114", 252 "8.8.8.8", 253 }, 254 }, 255 Profile: Profile{ 256 StoreSelected: true, 257 }, 258 } 259 260 if err := yaml.Unmarshal(buf, rawCfg); err != nil { 261 return nil, err 262 } 263 264 return rawCfg, nil 265 } 266 267 func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { 268 config := &Config{} 269 270 config.Experimental = &rawCfg.Experimental 271 config.Profile = &rawCfg.Profile 272 273 general, err := parseGeneral(rawCfg) 274 if err != nil { 275 return nil, err 276 } 277 config.General = general 278 279 config.Inbounds = rawCfg.Inbounds 280 281 proxies, providers, err := parseProxies(rawCfg) 282 if err != nil { 283 return nil, err 284 } 285 config.Proxies = proxies 286 config.Providers = providers 287 288 rules, err := parseRules(rawCfg, proxies) 289 if err != nil { 290 return nil, err 291 } 292 config.Rules = rules 293 294 hosts, err := parseHosts(rawCfg) 295 if err != nil { 296 return nil, err 297 } 298 config.Hosts = hosts 299 300 dnsCfg, err := parseDNS(rawCfg, hosts) 301 if err != nil { 302 return nil, err 303 } 304 config.DNS = dnsCfg 305 306 config.Users = parseAuthentication(rawCfg.Authentication) 307 308 config.Tunnels = rawCfg.Tunnels 309 // verify tunnels 310 for _, t := range config.Tunnels { 311 if _, ok := config.Proxies[t.Proxy]; !ok { 312 return nil, fmt.Errorf("tunnel proxy %s not found", t.Proxy) 313 } 314 } 315 316 return config, nil 317 } 318 319 func parseGeneral(cfg *RawConfig) (*General, error) { 320 externalUI := cfg.ExternalUI 321 322 // checkout externalUI exist 323 if externalUI != "" { 324 externalUI = C.Path.Resolve(externalUI) 325 326 if _, err := os.Stat(externalUI); os.IsNotExist(err) { 327 return nil, fmt.Errorf("external-ui: %s not exist", externalUI) 328 } 329 } 330 331 return &General{ 332 LegacyInbound: LegacyInbound{ 333 Port: cfg.Port, 334 SocksPort: cfg.SocksPort, 335 RedirPort: cfg.RedirPort, 336 TProxyPort: cfg.TProxyPort, 337 MixedPort: cfg.MixedPort, 338 AllowLan: cfg.AllowLan, 339 BindAddress: cfg.BindAddress, 340 }, 341 Controller: Controller{ 342 ExternalController: cfg.ExternalController, 343 ExternalUI: cfg.ExternalUI, 344 Secret: cfg.Secret, 345 }, 346 Mode: cfg.Mode, 347 LogLevel: cfg.LogLevel, 348 IPv6: cfg.IPv6, 349 Interface: cfg.Interface, 350 RoutingMark: cfg.RoutingMark, 351 }, nil 352 } 353 354 func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[string]providerTypes.ProxyProvider, err error) { 355 proxies = make(map[string]C.Proxy) 356 providersMap = make(map[string]providerTypes.ProxyProvider) 357 proxyList := []string{} 358 proxiesConfig := cfg.Proxy 359 groupsConfig := cfg.ProxyGroup 360 providersConfig := cfg.ProxyProvider 361 362 proxies["DIRECT"] = adapter.NewProxy(outbound.NewDirect()) 363 proxies["REJECT"] = adapter.NewProxy(outbound.NewReject()) 364 proxyList = append(proxyList, "DIRECT", "REJECT") 365 366 // parse proxy 367 for idx, mapping := range proxiesConfig { 368 proxy, err := adapter.ParseProxy(mapping) 369 if err != nil { 370 return nil, nil, fmt.Errorf("proxy %d: %w", idx, err) 371 } 372 373 if _, exist := proxies[proxy.Name()]; exist { 374 return nil, nil, fmt.Errorf("proxy %s is the duplicate name", proxy.Name()) 375 } 376 proxies[proxy.Name()] = proxy 377 proxyList = append(proxyList, proxy.Name()) 378 } 379 380 // keep the original order of ProxyGroups in config file 381 for idx, mapping := range groupsConfig { 382 groupName, existName := mapping["name"].(string) 383 if !existName { 384 return nil, nil, fmt.Errorf("proxy group %d: missing name", idx) 385 } 386 proxyList = append(proxyList, groupName) 387 } 388 389 // check if any loop exists and sort the ProxyGroups 390 if err := proxyGroupsDagSort(groupsConfig); err != nil { 391 return nil, nil, err 392 } 393 394 // parse and initial providers 395 for name, mapping := range providersConfig { 396 if name == provider.ReservedName { 397 return nil, nil, fmt.Errorf("can not defined a provider called `%s`", provider.ReservedName) 398 } 399 400 pd, err := provider.ParseProxyProvider(name, mapping) 401 if err != nil { 402 return nil, nil, fmt.Errorf("parse proxy provider %s error: %w", name, err) 403 } 404 405 providersMap[name] = pd 406 } 407 408 for _, provider := range providersMap { 409 log.Infoln("Start initial provider %s", provider.Name()) 410 if err := provider.Initial(); err != nil { 411 return nil, nil, fmt.Errorf("initial proxy provider %s error: %w", provider.Name(), err) 412 } 413 } 414 415 // parse proxy group 416 for idx, mapping := range groupsConfig { 417 group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap) 418 if err != nil { 419 return nil, nil, fmt.Errorf("proxy group[%d]: %w", idx, err) 420 } 421 422 groupName := group.Name() 423 if _, exist := proxies[groupName]; exist { 424 return nil, nil, fmt.Errorf("proxy group %s: the duplicate name", groupName) 425 } 426 427 proxies[groupName] = adapter.NewProxy(group) 428 } 429 430 // initial compatible provider 431 for _, pd := range providersMap { 432 if pd.VehicleType() != providerTypes.Compatible { 433 continue 434 } 435 436 log.Infoln("Start initial compatible provider %s", pd.Name()) 437 if err := pd.Initial(); err != nil { 438 return nil, nil, err 439 } 440 } 441 442 ps := []C.Proxy{} 443 for _, v := range proxyList { 444 ps = append(ps, proxies[v]) 445 } 446 hc := provider.NewHealthCheck(ps, "", 0, true) 447 pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc) 448 providersMap[provider.ReservedName] = pd 449 450 global := outboundgroup.NewSelector( 451 &outboundgroup.GroupCommonOption{ 452 Name: "GLOBAL", 453 }, 454 []providerTypes.ProxyProvider{pd}, 455 ) 456 proxies["GLOBAL"] = adapter.NewProxy(global) 457 return proxies, providersMap, nil 458 } 459 460 func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) { 461 rules := []C.Rule{} 462 rulesConfig := cfg.Rule 463 464 // parse rules 465 for idx, line := range rulesConfig { 466 rule := trimArr(strings.Split(line, ",")) 467 var ( 468 payload string 469 target string 470 params = []string{} 471 ) 472 473 switch l := len(rule); { 474 case l == 2: 475 target = rule[1] 476 case l == 3: 477 payload = rule[1] 478 target = rule[2] 479 case l >= 4: 480 payload = rule[1] 481 target = rule[2] 482 params = rule[3:] 483 default: 484 return nil, fmt.Errorf("rules[%d] [%s] error: format invalid", idx, line) 485 } 486 487 if _, ok := proxies[target]; !ok { 488 return nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target) 489 } 490 491 rule = trimArr(rule) 492 params = trimArr(params) 493 494 parsed, parseErr := R.ParseRule(rule[0], payload, target, params) 495 if parseErr != nil { 496 return nil, fmt.Errorf("rules[%d] [%s] error: %s", idx, line, parseErr.Error()) 497 } 498 499 rules = append(rules, parsed) 500 } 501 502 return rules, nil 503 } 504 505 func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) { 506 tree := trie.New() 507 508 // add default hosts 509 if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil { 510 log.Errorln("insert localhost to host error: %s", err.Error()) 511 } 512 513 if len(cfg.Hosts) != 0 { 514 for domain, ipStr := range cfg.Hosts { 515 ip := net.ParseIP(ipStr) 516 if ip == nil { 517 return nil, fmt.Errorf("%s is not a valid IP", ipStr) 518 } 519 tree.Insert(domain, ip) 520 } 521 } 522 523 return tree, nil 524 } 525 526 func hostWithDefaultPort(host string, defPort string) (string, error) { 527 if !strings.Contains(host, ":") { 528 host += ":" 529 } 530 531 hostname, port, err := net.SplitHostPort(host) 532 if err != nil { 533 return "", err 534 } 535 536 if port == "" { 537 port = defPort 538 } 539 540 return net.JoinHostPort(hostname, port), nil 541 } 542 543 func parseNameServer(servers []string) ([]dns.NameServer, error) { 544 nameservers := []dns.NameServer{} 545 546 for idx, server := range servers { 547 // parse without scheme .e.g 8.8.8.8:53 548 if !strings.Contains(server, "://") { 549 server = "udp://" + server 550 } 551 u, err := url.Parse(server) 552 if err != nil { 553 return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) 554 } 555 556 // parse with specific interface 557 // .e.g 10.0.0.1#en0 558 interfaceName := u.Fragment 559 560 var addr, dnsNetType string 561 switch u.Scheme { 562 case "udp": 563 addr, err = hostWithDefaultPort(u.Host, "53") 564 dnsNetType = "" // UDP 565 case "tcp": 566 addr, err = hostWithDefaultPort(u.Host, "53") 567 dnsNetType = "tcp" // TCP 568 case "tls": 569 addr, err = hostWithDefaultPort(u.Host, "853") 570 dnsNetType = "tcp-tls" // DNS over TLS 571 case "https": 572 clearURL := url.URL{Scheme: "https", Host: u.Host, Path: u.Path, User: u.User} 573 addr = clearURL.String() 574 dnsNetType = "https" // DNS over HTTPS 575 case "dhcp": 576 addr = u.Host 577 dnsNetType = "dhcp" // UDP from DHCP 578 default: 579 return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme) 580 } 581 582 if err != nil { 583 return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) 584 } 585 586 nameservers = append( 587 nameservers, 588 dns.NameServer{ 589 Net: dnsNetType, 590 Addr: addr, 591 Interface: interfaceName, 592 }, 593 ) 594 } 595 return nameservers, nil 596 } 597 598 func parseNameServerPolicy(nsPolicy map[string]string) (map[string]dns.NameServer, error) { 599 policy := map[string]dns.NameServer{} 600 601 for domain, server := range nsPolicy { 602 nameservers, err := parseNameServer([]string{server}) 603 if err != nil { 604 return nil, err 605 } 606 if _, valid := trie.ValidAndSplitDomain(domain); !valid { 607 return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", domain) 608 } 609 policy[domain] = nameservers[0] 610 } 611 612 return policy, nil 613 } 614 615 func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) { 616 ipNets := []*net.IPNet{} 617 618 for idx, ip := range ips { 619 _, ipnet, err := net.ParseCIDR(ip) 620 if err != nil { 621 return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error()) 622 } 623 ipNets = append(ipNets, ipnet) 624 } 625 626 return ipNets, nil 627 } 628 629 func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie) (*DNS, error) { 630 cfg := rawCfg.DNS 631 if cfg.Enable && len(cfg.NameServer) == 0 { 632 return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") 633 } 634 635 dnsCfg := &DNS{ 636 Enable: cfg.Enable, 637 Listen: cfg.Listen, 638 IPv6: lo.FromPtrOr(cfg.IPv6, rawCfg.IPv6), 639 EnhancedMode: cfg.EnhancedMode, 640 FallbackFilter: FallbackFilter{ 641 IPCIDR: []*net.IPNet{}, 642 }, 643 } 644 var err error 645 if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer); err != nil { 646 return nil, err 647 } 648 649 if dnsCfg.Fallback, err = parseNameServer(cfg.Fallback); err != nil { 650 return nil, err 651 } 652 653 if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy); err != nil { 654 return nil, err 655 } 656 657 if len(cfg.DefaultNameserver) == 0 { 658 return nil, errors.New("default nameserver should have at least one nameserver") 659 } 660 if dnsCfg.DefaultNameserver, err = parseNameServer(cfg.DefaultNameserver); err != nil { 661 return nil, err 662 } 663 // check default nameserver is pure ip addr 664 for _, ns := range dnsCfg.DefaultNameserver { 665 host, _, err := net.SplitHostPort(ns.Addr) 666 if err != nil || net.ParseIP(host) == nil { 667 return nil, errors.New("default nameserver should be pure IP") 668 } 669 } 670 671 if cfg.EnhancedMode == C.DNSFakeIP { 672 _, ipnet, err := net.ParseCIDR(cfg.FakeIPRange) 673 if err != nil { 674 return nil, err 675 } 676 677 var host *trie.DomainTrie 678 // fake ip skip host filter 679 if len(cfg.FakeIPFilter) != 0 { 680 host = trie.New() 681 for _, domain := range cfg.FakeIPFilter { 682 host.Insert(domain, true) 683 } 684 } 685 686 pool, err := fakeip.New(fakeip.Options{ 687 IPNet: ipnet, 688 Size: 1000, 689 Host: host, 690 Persistence: rawCfg.Profile.StoreFakeIP, 691 }) 692 if err != nil { 693 return nil, err 694 } 695 696 dnsCfg.FakeIPRange = pool 697 } 698 699 dnsCfg.FallbackFilter.GeoIP = cfg.FallbackFilter.GeoIP 700 dnsCfg.FallbackFilter.GeoIPCode = cfg.FallbackFilter.GeoIPCode 701 if fallbackip, err := parseFallbackIPCIDR(cfg.FallbackFilter.IPCIDR); err == nil { 702 dnsCfg.FallbackFilter.IPCIDR = fallbackip 703 } 704 dnsCfg.FallbackFilter.Domain = cfg.FallbackFilter.Domain 705 706 if cfg.UseHosts { 707 dnsCfg.Hosts = hosts 708 } 709 710 if len(cfg.SearchDomains) != 0 { 711 for _, domain := range cfg.SearchDomains { 712 if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") { 713 return nil, errors.New("search domains should not start or end with '.'") 714 } 715 if strings.Contains(domain, ":") { 716 return nil, errors.New("search domains are for ipv4 only and should not contain ports") 717 } 718 } 719 dnsCfg.SearchDomains = cfg.SearchDomains 720 } 721 722 return dnsCfg, nil 723 } 724 725 func parseAuthentication(rawRecords []string) []auth.AuthUser { 726 users := []auth.AuthUser{} 727 for _, line := range rawRecords { 728 if user, pass, found := strings.Cut(line, ":"); found { 729 users = append(users, auth.AuthUser{User: user, Pass: pass}) 730 } 731 } 732 return users 733 }