package main import ( "encoding/json" "errors" "fmt" "io/ioutil" "log" "net" "net/http" "net/url" "strings" "time" "github.com/go-ping/ping" "github.com/miekg/dns" "git.nemunai.re/srs/adlin/libadlin" ) const ( DEFAULT_RESOLVER = "2a01:e0a:2b:2250::1" year68 = 1 << 31 // For RFC1982 (Serial Arithmetic) calculations in 32 bits. Taken from miekg/dns ) var ( verbose = false verbose2 = false domainsHostingMap = map[string]string{} ) // ICMP func check_ping(ip string, cb func(pkt *ping.Packet)) (err error) { var pinger *ping.Pinger pinger, err = ping.NewPinger(ip) if err != nil { if verbose { log.Printf("check_ping: %s: %s", ip, err.Error()) } return } defer pinger.Stop() pinger.Timeout = time.Second * 5 pinger.Count = 1 pinger.OnRecv = cb pinger.SetPrivileged(true) err = pinger.Run() return } // PORT 53 func get_GLUE(student *adlin.Student) (aaaa net.IP, err error) { client := dns.Client{Net: "tcp", Timeout: time.Second * 5} domain := student.MyDelegatedDomain() dnssrv := "[2a01:e0a:2b:2250::b]:53" if strings.HasSuffix(domain, student.MyDelegatedDomainSuffix()) { dnssrv = "[2a01:e0a:2b:2250::b]:53" } else if v, ok := domainsHostingMap[domain]; ok { dnssrv = v } else { // Looking for root NS m := new(dns.Msg) m.SetQuestion(".", dns.TypeNS) m.RecursionDesired = false m.SetEdns0(4096, true) var r *dns.Msg r, _, err = client.Exchange(m, dnssrv) if err != nil { return } if r == nil { return nil, errors.New("response is nil during initial recursion") } if r.Rcode != dns.RcodeSuccess { return nil, errors.New("failed to get a valid answer during initial recursion") } for _, answer := range r.Answer { if t, ok := answer.(*dns.NS); ok { dnssrv = t.Ns + ":53" } } // Do casual recursion i := 0 recursion: for i = 0; i < 10; i++ { m := new(dns.Msg) m.SetQuestion(domain, dns.TypeNS) m.RecursionDesired = false m.SetEdns0(4096, true) var r *dns.Msg r, _, err = client.Exchange(m, dnssrv) if err != nil { return } if r == nil { return nil, errors.New("response is nil during recursion") } if r.Rcode != dns.RcodeSuccess { return nil, errors.New("failed to get a valid answer during recursion") } for _, answer := range r.Ns { if t, ok := answer.(*dns.NS); ok { dnssrv = t.Ns + ":53" if t.Header().Name == domain { break recursion } } } } if i >= 10 { return nil, fmt.Errorf("too much name recursions") } else { domainsHostingMap[domain] = dnssrv } } m := new(dns.Msg) m.SetQuestion(domain, dns.TypeNS) m.RecursionDesired = false m.SetEdns0(4096, true) var r *dns.Msg r, _, err = client.Exchange(m, dnssrv) if err != nil { return } if r == nil { return nil, errors.New("response is nil") } if r.Rcode != dns.RcodeSuccess { return nil, errors.New("failed to get a valid answer") } for _, extra := range r.Extra { if t, ok := extra.(*dns.AAAA); ok { aaaa = t.AAAA } } return } func check_dns(domain, ip string) (aaaa net.IP, err error) { client := dns.Client{Timeout: time.Second * 5} m := new(dns.Msg) m.SetQuestion(domain, dns.TypeAAAA) var r *dns.Msg r, _, err = client.Exchange(m, fmt.Sprintf("[%s]:53", ip)) if err != nil { return } if r == nil { err = errors.New("response is nil") return } if r.Rcode != dns.RcodeSuccess { err = errors.New("failed to get a valid answer") return } for _, answer := range r.Answer { if t, ok := answer.(*dns.AAAA); ok { aaaa = t.AAAA } } return } func check_dnssec(domain, ip string) (err error) { client := dns.Client{Net: "tcp", Timeout: time.Second * 10} // Get DNSKEY m := new(dns.Msg) m.SetEdns0(4096, true) m.SetQuestion(domain, dns.TypeDNSKEY) var r *dns.Msg r, _, err = client.Exchange(m, fmt.Sprintf("[%s]:53", ip)) if err != nil { return } if r == nil { return errors.New("response is nil") } if r.Rcode != dns.RcodeSuccess { return errors.New("failed to get a valid answer when getting DNSKEY") } var rrs []dns.RR var dnskeys []*dns.DNSKEY var dnskeysig *dns.RRSIG for _, answer := range r.Answer { if t, ok := answer.(*dns.DNSKEY); ok { dnskeys = append(dnskeys, t) rrs = append(rrs, dns.RR(t)) } else if t, ok := answer.(*dns.RRSIG); ok { dnskeysig = t } } if dnskeysig == nil { return fmt.Errorf("Unable to verify DNSKEY record signature: No RRSIG found for DNSKEY record.") } found := false for _, dnskey := range dnskeys { if err = dnskeysig.Verify(dnskey, rrs); err == nil { found = true break } } if !found { return fmt.Errorf("Unable to verify DNSKEY record signature: %w", err) } // Check AAAA validity m = new(dns.Msg) m.SetEdns0(4096, true) m.SetQuestion(domain, dns.TypeAAAA) r, _, err = client.Exchange(m, fmt.Sprintf("[%s]:53", ip)) if err != nil { return } if r == nil { return errors.New("response is nil") } if r.Rcode != dns.RcodeSuccess { return errors.New("failed to get a valid answer when getting AAAA records") } rrs = []dns.RR{} var aaaas []*dns.AAAA var aaaasig *dns.RRSIG for _, answer := range r.Answer { if t, ok := answer.(*dns.AAAA); ok { aaaas = append(aaaas, t) rrs = append(rrs, t) } else if t, ok := answer.(*dns.RRSIG); ok { aaaasig = t } } if len(aaaas) == 0 { return errors.New("Something odd happen: no AAAA record found.") } if aaaasig == nil { return fmt.Errorf("Unable to verify AAAA record signature: No RRSIG found for AAAA record.") } found = false for _, dnskey := range dnskeys { if err = aaaasig.Verify(dnskey, rrs); err == nil { found = true if !aaaasig.ValidityPeriod(time.Now()) { utc := time.Now().UTC().Unix() modi := (int64(aaaasig.Inception) - utc) / year68 ti := int64(aaaasig.Inception) + modi*year68 mode := (int64(aaaasig.Expiration) - utc) / year68 te := int64(aaaasig.Expiration) + mode*year68 if ti > utc { return fmt.Errorf("Unable to verify AAAA record signature: signature not yet valid") } else if utc > te { return fmt.Errorf("Unable to verify AAAA record signature: signature expired") } else { return fmt.Errorf("Unable to verify AAAA record signature: signature expired or not yet valid") } } break } } if !found { return fmt.Errorf("Unable to verify AAAA record signature: %w", err) } // Check DS m = new(dns.Msg) m.SetQuestion(domain, dns.TypeDS) m.RecursionDesired = false m.SetEdns0(4096, true) r, _, err = client.Exchange(m, "[2a01:e0a:2b:2250::b]:53") if err != nil { return } if r == nil { return errors.New("response is nil") } if r.Rcode != dns.RcodeSuccess { return errors.New("failed to get a valid answer when getting DS records in parent server") } found = false for _, answer := range r.Answer { if t, ok := answer.(*dns.DS); ok { for _, dnskey := range dnskeys { expectedDS := dnskey.ToDS(dns.SHA256) if expectedDS.KeyTag == t.KeyTag && expectedDS.Algorithm == t.Algorithm && expectedDS.DigestType == t.DigestType && expectedDS.Digest == t.Digest { found = true err = nil break } else { err = fmt.Errorf("DS record found in parent zone differs from DNSKEY %v vs. %v.", expectedDS, t) } } } } if !found { if err == nil { return fmt.Errorf("Unable to find a valid DS record in parent zone (if you use your own domain (ie. not given by maatma), this can be due to a previously cached response, you should wait).") } else { return err } } return } // PORT 80 func check_http(ip, dn string) (err error) { client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } req, errr := http.NewRequest("GET", fmt.Sprintf("http://[%s]/", ip), nil) if errr != nil { return errr } if dn != "" { req.Header.Add("Host", strings.TrimSuffix(dn, ".")) } var resp *http.Response resp, err = client.Do(req) if err != nil { return } defer resp.Body.Close() if dn != "" && resp.StatusCode >= 400 { return fmt.Errorf("Bad status, got: %d (%s)", resp.StatusCode, resp.Status) } _, err = ioutil.ReadAll(resp.Body) return } // PORT 443 func check_https(domain, ip string) (err error) { var resp *http.Response resp, err = http.Get(fmt.Sprintf("https://%s/", strings.TrimSuffix(domain, "."))) if err != nil { return } defer resp.Body.Close() if resp.StatusCode >= 300 && resp.StatusCode < 400 { loc := resp.Header.Get("Location") if loc != "" && strings.HasSuffix(dns.Fqdn(loc), domain) { if dns.Fqdn(loc) == domain { return fmt.Errorf("Redirection loop %s redirect to %s", domain, loc) } else if err = check_https(dns.Fqdn(loc), ip); err != nil { return fmt.Errorf("Error after following redirection to %s: %w", loc, err) } else { return } } } if resp.StatusCode >= 300 { return fmt.Errorf("Bad status, got: %d (%s)", resp.StatusCode, resp.Status) } _, err = ioutil.ReadAll(resp.Body) return } // MATRIX type matrix_result struct { WellKnownResult struct { Server string `json:"m.server"` Result string `json:"result"` } DNSResult struct { SRVError *struct { Message string } } ConnectionReports map[string]struct { Errors []string } ConnectionErrors map[string]struct { Message string } Version struct { Name string `json:"name"` Version string `json:"version"` } FederationOK bool `json:"FederationOK"` } func check_matrix_federation(domain string) (version string, err error) { var resp *http.Response resp, err = http.Get(fmt.Sprintf("https://federation-tester.adlin.nemunai.re/api/report?server_name=%s", strings.TrimSuffix(domain, "."))) if err != nil { return } defer resp.Body.Close() if resp.StatusCode >= 300 { return "", fmt.Errorf("Sorry, the federation tester is broken. Check on https://federationtester.matrix.org/#%s", strings.TrimSuffix(domain, ".")) } var federationTest matrix_result if err = json.NewDecoder(resp.Body).Decode(&federationTest); err != nil { log.Printf("Error in check_matrix_federation, when decoding json: %s", err.Error()) return "", fmt.Errorf("Sorry, the federation tester is broken. Check on https://federationtester.matrix.org/#%s", strings.TrimSuffix(domain, ".")) } else if federationTest.FederationOK { version = federationTest.Version.Name + " " + federationTest.Version.Version return version, nil } else if federationTest.DNSResult.SRVError != nil && federationTest.WellKnownResult.Result != "" { return "", fmt.Errorf("%s OR %s", federationTest.DNSResult.SRVError.Message, federationTest.WellKnownResult.Result) } else if len(federationTest.ConnectionErrors) > 0 { var msg strings.Builder for srv, cerr := range federationTest.ConnectionErrors { if msg.Len() > 0 { msg.WriteString("; ") } msg.WriteString(srv) msg.WriteString(": ") msg.WriteString(cerr.Message) } return "", fmt.Errorf("Connection errors: %s", msg.String()) } else if federationTest.WellKnownResult.Server != strings.TrimSuffix(domain, ".") { return "", fmt.Errorf("Bad homeserver_name: got %s, expected %s.", federationTest.WellKnownResult.Server, strings.TrimSuffix(domain, ".")) } else { return "", fmt.Errorf("An unimplemented error occurs. Please report to nemunaire. But know that federation seems to be broken. Check https://federationtester.matrix.org/#%s", strings.TrimSuffix(domain, ".")) } } type matrix_wk_client struct { Homeserver struct { BaseURL string `json:"base_url"` } `json:"m.homeserver"` IdentityServer struct { BaseURL string `json:"base_url"` } `json:"m.identity_server"` } type matrix_client_versions struct { Versions []string `json:"versions"` UnstableFeatures map[string]bool `json:"unstable_features"` } func check_matrix_client(domain string) (version string, err error) { var resp *http.Response resp, err = http.Get(fmt.Sprintf("https://%s/.well-known/matrix/client", strings.TrimSuffix(domain, "."))) if err != nil { return } defer resp.Body.Close() var HomeserverBase = fmt.Sprintf("https://%s", strings.TrimSuffix(domain, ".")) if resp.StatusCode < 300 { var wellknown matrix_wk_client if err = json.NewDecoder(resp.Body).Decode(&wellknown); err != nil { log.Printf("Error in check_matrix_client, when decoding json: %s", err.Error()) return "", fmt.Errorf("File at https://%s/.well-known/matrix/client is invalid: JSON parse error", strings.TrimSuffix(domain, ".")) } else if wellknown.Homeserver.BaseURL != "" { if baseurl, err := url.Parse(wellknown.Homeserver.BaseURL); err != nil { return "", fmt.Errorf("File at https://%s/.well-known/matrix/client is invalid: Bad homeserver URL: %s", strings.TrimSuffix(domain, "."), err.Error()) } else if !strings.HasSuffix(strings.TrimSuffix(baseurl.Host, "."), strings.TrimSuffix(domain, ".")) { return "", fmt.Errorf("Your homeserver base_url is not under %s", strings.TrimSuffix(domain, ".")) } else if strings.TrimSuffix(baseurl.Host, ".") == strings.TrimSuffix(domain, ".") { // This test can be optional return "", fmt.Errorf("Your homeserver should be on its own subdomain") } else { HomeserverBase = wellknown.Homeserver.BaseURL } } } var resp2 *http.Response resp2, err = http.Get(fmt.Sprintf("%s/_matrix/client/versions", HomeserverBase)) if err != nil { return } defer resp2.Body.Close() if resp2.StatusCode != http.StatusOK { return "", fmt.Errorf("Unable to fetch your homeserver versions at %s/_matrix/client/versions: %s", HomeserverBase, resp2.Status) } var clientTest matrix_client_versions if err = json.NewDecoder(resp2.Body).Decode(&clientTest); err != nil { log.Printf("Error in check_matrix_client, when decoding versions json: %s", err.Error()) return "", fmt.Errorf("File at %s/_matrix/client/versions is invalid: JSON parse error: %s", HomeserverBase, err.Error()) } else if len(clientTest.Versions) == 0 { return "", fmt.Errorf("File at %s/_matrix/client/versions is invalid: no protocol version supported", HomeserverBase) } else { return clientTest.Versions[len(clientTest.Versions)-1], nil } } // Main func minTunnelVersion(std *adlin.Student, suffixip int) (int, error) { tunnels, err := std.GetTunnelTokens() if err != nil { return 0, err } var minversion int = 2147483647 for _, tunnel := range tunnels { if tunnel.Version == 0 { continue } if tunnel.Dump != nil && tunnel.Version < minversion && suffixip == tunnel.SuffixIP { minversion = tunnel.Version } } return minversion, nil } func studentChecker(std *adlin.Student, also_check_matrix bool, offline bool) { tuns, err := std.GetActivesTunnels() if err != nil { if offline { tuns, err = std.GetDefaultTunnels() if err != nil { if verbose { log.Printf("SKip %s as I'm unable to generate default tunnels: %s", std.Login, err.Error()) } return } } else { if verbose2 { log.Printf("SKip %s due to error when getting active tunnels: %s", std.Login, err.Error()) } return } } if verbose2 && len(tuns) == 0 { log.Printf("%s has no active tunnels: %s", std.Login, err.Error()) } for _, tun := range tuns { stdIP := tun.GetStudentIP() if verbose2 { log.Printf("Tests %s on %s...", std.Login, stdIP) } // Check ping err = check_ping(stdIP, func(pkt *ping.Packet) { tunnel_version, err := minTunnelVersion(std, tun.SuffixIP) if verbose { log.Printf("%s PONG (on %x); version=%d (%v)\n", std.Login, tun.SuffixIP, tunnel_version, err) } std.OnPong(true) if !offline && (tunnel_version == 2147483647 || tunnel_version == 0) { log.Printf("%s unknown tunnel version: %d skipping tests (%v)", std.Login, tunnel_version, err) return } // PingResolver if tunnel_version == 3 { tmp := strings.Split(stdIP, ":") tmp[len(tmp)-1] = "2" stdResolverIP := strings.Join(tmp, ":") go check_ping(stdResolverIP, func(_ *ping.Packet) { if verbose { log.Printf("%s resolver PONG", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][PingResolver], ""); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } }) } dnsIP := stdIP var glueErr error // Is GLUE defined? if glueIP, err := get_GLUE(std); glueIP != nil { dnsIP = glueIP.String() if verbose { log.Printf("%s has defined GLUE: %s\n", std.Login, dnsIP) } } else if err != nil { log.Printf("%s and GLUE: %s\n", std.Login, err) glueErr = err } snicheck1 := false snicheck1_tested := false // Check DNS if addr, err := check_dns(std.MyDelegatedDomain(), dnsIP); err == nil { if addr == nil { dnsAt := " at " + dnsIP if glueErr != nil { dnsAt = " + there is a problem with the GLUE record: " + glueErr.Error() } if errreg := std.RegisterChallengeError(CheckMap[tunnel_version][DNSDelegation], fmt.Errorf("%s: empty response from the server%s", std.MyDelegatedDomain(), dnsAt)); errreg != nil { log.Printf("Unable to register challenge error for %s: %s\n", std.Login, errreg) } } else { if verbose { log.Printf("%s just unlocked DNS challenge\n", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][DNSDelegation], addr.String()); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } // Check HTTP with DNS if glueErr != nil { std.RegisterChallengeError(CheckMap[tunnel_version][HTTPonDelegatedDomain], fmt.Errorf("Unable to perform the test due to GLUE problem: %w", glueErr)) } else if err := check_http(addr.String(), std.MyDelegatedDomain()); err == nil { if verbose { log.Printf("%s just unlocked HTTP challenge\n", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][HTTPonDelegatedDomain], ""); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } } else { std.RegisterChallengeError(CheckMap[tunnel_version][HTTPonDelegatedDomain], err) if verbose { log.Printf("%s and HTTP (with DNS ip=%s): %s\n", std.Login, addr.String(), err) } } // Check HTTPs with DNS if glueErr != nil { std.RegisterChallengeError(CheckMap[tunnel_version][HTTPSonDelegatedDomain], fmt.Errorf("Unable to perform the test due to GLUE problem: %w", glueErr)) } else if err := check_https(std.MyDelegatedDomain(), addr.String()); err == nil { snicheck1 = true snicheck1_tested = true if verbose { log.Printf("%s just unlocked HTTPS challenge\n", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][HTTPSonDelegatedDomain], ""); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } } else { snicheck1_tested = true std.RegisterChallengeError(CheckMap[tunnel_version][HTTPSonDelegatedDomain], err) if verbose { log.Printf("%s and HTTPS (with DNS ip=%s): %s\n", std.Login, addr.String(), err) } } // Check Matrix (only if GLUE Ok and defer contraint) if glueErr == nil && also_check_matrix { // Check Matrix Federation first if v, err := check_matrix_federation(std.MyDelegatedDomain()); err == nil { if verbose { log.Printf("%s just unlocked Matrix federation challenge\n", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][MatrixSrv], v); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } } else { std.RegisterChallengeError(CheckMap[tunnel_version][MatrixSrv], err) if verbose { log.Printf("%s and Matrix federation: %s\n", std.Login, err) } } // Check Matrix Client if v, err := check_matrix_client(std.MyDelegatedDomain()); err == nil { if verbose { log.Printf("%s just unlocked Matrix client challenge\n", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][MatrixClt], v); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } } else { std.RegisterChallengeError(CheckMap[tunnel_version][MatrixClt], err) if verbose { log.Printf("%s and Matrix client: %s\n", std.Login, err) } } } // Check DNSSEC (only if GLUE Ok) if glueErr == nil { if err := check_dnssec(std.MyDelegatedDomain(), dnsIP); err == nil { if verbose { log.Printf("%s just unlocked DNSSEC challenge\n", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][DNSSEC], ""); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } } else { std.RegisterChallengeError(CheckMap[tunnel_version][DNSSEC], err) if verbose { log.Printf("%s and DNSSEC: %s\n", std.Login, err) } } } } } else { if errreg := std.RegisterChallengeError(CheckMap[tunnel_version][DNSDelegation], err); errreg != nil { log.Printf("Unable to register challenge error for %s: %s\n", std.Login, errreg) } if verbose { log.Printf("%s and DNS: %s\n", std.Login, err) } } // Check HTTP without DNS if err := check_http(stdIP, ""); err == nil { if verbose { log.Printf("%s just unlocked HTTP IP (without DNS) challenge\n", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][HTTPonIP], ""); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } } else { std.RegisterChallengeError(CheckMap[tunnel_version][HTTPonIP], err) if verbose { log.Printf("%s and HTTP IP (without DNS): %s\n", std.Login, err) } } // Check DNS for association if addr, err := check_dns(std.MyAssociatedDomain(), DEFAULT_RESOLVER); err == nil { // Check HTTP on delegated domain if err := check_http(addr.String(), std.MyAssociatedDomain()); err == nil { if verbose { log.Printf("%s just unlocked HTTP (without DNS) challenge\n", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][HTTPonAssociatedDomain], ""); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } } else { std.RegisterChallengeError(CheckMap[tunnel_version][HTTPonAssociatedDomain], err) if verbose { log.Printf("%s and HTTP (without DNS): %s\n", std.Login, err) } } // Check HTTPs without DNS if err := check_https(std.MyAssociatedDomain(), stdIP); err == nil { if verbose { log.Printf("%s just unlocked HTTPS challenge\n", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][HTTPSonAssociatedDomain], ""); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } // SNI check: validate if this check + HTTPS on delegation is validated if snicheck1 { if verbose { log.Printf("%s just unlocked HTTPS-SNI challenge\n", std.Login) } if _, err := std.UnlockChallenge(CheckMap[tunnel_version][HTTPSSNI], ""); err != nil { log.Printf("Unable to register challenge for %s: %s\n", std.Login, err.Error()) } } } else { std.RegisterChallengeError(CheckMap[tunnel_version][HTTPSonAssociatedDomain], err) if verbose { log.Printf("%s and HTTPS (without DNS): %s\n", std.Login, err) } } } if snicheck1_tested && !snicheck1 { std.RegisterChallengeError(CheckMap[tunnel_version][HTTPSSNI], fmt.Errorf("associated and delegated domain are not accessible through HTTPS at the same time, see errors for thoses checks")) if verbose { log.Printf("%s and HTTPS-SNI: %s\n", std.Login, "associated and delegated domains not accessible at the same time through HTTPS") } } return }) if err != nil && verbose { log.Printf("%s: Unable to perform ping to %s: %s", std.Login, stdIP, err.Error()) } } } func studentsChecker(offline bool) { students, err := adlin.GetStudents() if err != nil { log.Println("Unable to check students:", err) return } check_matrix_for := (time.Now().Second()/30)*5 + time.Now().Minute()%5 log.Printf("Checking students... (std_matrix%%10=%d)\n", check_matrix_for) for istd, s := range students { time.Sleep(250 * time.Millisecond) go studentChecker(s, istd%10 == check_matrix_for, offline) } }