This repository has been archived on 2024-03-03. You can view files and clone it, but cannot push or open issues or pull requests.
adlin/checker/checks.go

647 lines
16 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/miekg/dns"
ping "github.com/prometheus-community/pro-bing"
"git.nemunai.re/srs/adlin/libadlin"
)
type AdlinTest int
const (
Firewalled AdlinTest = iota
HTTPonIP
HTTPonAssociatedDomain
HTTPSonAssociatedDomain
DNSDelegation
HTTPonDelegatedDomain
HTTPSonDelegatedDomain
HTTPSSNI
MatrixSrv
MatrixClt
SNI
DNSSEC
PingResolver
DHCPonRH
DHCPonDG
DHCPonCM
DHCPonGuests
RHaccessNews
RHaccessNet
GuestNet
)
var CheckMap = map[int]map[AdlinTest]int{
2: map[AdlinTest]int{
Firewalled: 100,
HTTPonIP: 101,
HTTPonAssociatedDomain: 102,
HTTPSonAssociatedDomain: 103,
DNSDelegation: 104,
HTTPonDelegatedDomain: 105,
HTTPSonDelegatedDomain: 106,
HTTPSSNI: 107,
DNSSEC: 110,
},
/*2: map[AdlinTest]int{
Firewalled: 200,
HTTPonIP: 201,
HTTPonAssociatedDomain: 202,
HTTPSonAssociatedDomain: 203,
DNSDelegation: 204,
HTTPonDelegatedDomain: 205,
HTTPSonDelegatedDomain: 206,
HTTPSSNI: 207,
MatrixSrv: 208,
MatrixClt: 209,
DNSSEC: 210,
},*/
3: map[AdlinTest]int{
PingResolver: 300,
HTTPonIP: 301,
DNSDelegation: 303,
HTTPonDelegatedDomain: 304,
HTTPSonDelegatedDomain: 305,
MatrixSrv: 306,
MatrixClt: 307,
DHCPonRH: 308,
DHCPonDG: 309,
DHCPonCM: 310,
DHCPonGuests: 313,
RHaccessNews: 311,
RHaccessNet: 312,
GuestNet: 314,
},
}
func has_test(m map[AdlinTest]int, test AdlinTest) bool {
for k := range m {
if k == test {
return true
}
}
return false
}
// ICMP
func check_ping(ip string, cb func(pkt *ping.Packet), cb_finished func(stats *ping.Statistics)) (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.OnFinish = cb_finished
pinger.SetPrivileged(true)
err = pinger.Run()
return
}
func check_firewall(network, ip string) error {
port := rand.Int31n(64500) + 1024
conn, err := net.DialTimeout(network, fmt.Sprintf("[%s]:%d", ip, port), 3*time.Second)
if err != nil {
if operr, ok := err.(*net.OpError); ok && operr.Timeout() {
// We expect a timeout here if the firewall is well setuped
return nil
}
return fmt.Errorf("Port %d is not filtered: %s", port, err.Error())
}
conn.Close()
return fmt.Errorf("Port %d is open", port)
}
// 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:518:830::e]:53"
if strings.HasSuffix(domain, student.MyDelegatedDomainSuffix()) {
dnssrv = "[2a01:e0a:518:830::e]: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(proto, domain, ip string) (aaaa net.IP, err error) {
client := dns.Client{Net: proto, 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_dns_both(domain, ip string) (aaaa net.IP, err error) {
if aaaa, err = check_dns_udp(domain, ip); err != nil {
err = fmt.Errorf("dig @%s %s: %w", ip, domain, err)
return
}
if aaaa, err = check_dns_tcp(domain, ip); err != nil {
err = fmt.Errorf("Test over TCP: dig +tcp @%s %s: %w", ip, domain, err)
return
}
return
}
func check_dns_udp(domain, ip string) (net.IP, error) {
return check_dns("", domain, ip)
}
func check_dns_tcp(domain, ip string) (net.IP, error) {
return check_dns("tcp", domain, ip)
}
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:518:830::e]: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 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)); 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
}
}