Skip to content

Commit

Permalink
feat: clash
Browse files Browse the repository at this point in the history
Signed-off-by: Birkhoff Lee <git@birkhoff.me>
  • Loading branch information
BirkhoffLee committed Apr 12, 2024
1 parent be48367 commit 92a79cb
Show file tree
Hide file tree
Showing 9 changed files with 481 additions and 251 deletions.
123 changes: 123 additions & 0 deletions checks/clash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package checks

import (
"encoding/json"
"fmt"
"net/http"
"strings"
"regexp"
"net"
)

type ClashAPIRootResponse struct {
Hello string `json:"hello,omitempty"` // clash
Message string `json:"message,omitempty"` // Unauthorized
}

func lookupFirstIp4Address(domain string) (string, error) {
// lookup first ipv4 address of a domain
addrs, err := net.LookupHost(domain)

if err != nil {
return "", err
}

for _, addr := range addrs {
if ip := net.ParseIP(addr); ip.To4() != nil {
return addr, nil
}
}

return "", fmt.Errorf("no ipv4 address found")
}

func IsRandomDomainResolvedToClashAddressSpace() (bool) {
addr, err := lookupFirstIp4Address("this.domain.does.not.exist.")

if err != nil {
return false
}

return strings.HasPrefix(addr, "198.18.")
}

func GetClashApiBaseUrl() (string, error) {
// Get Clash endpoint from either default gateway or system proxy
addr := ""

if proxy, err := getSystemDefaultProxy(); err == nil {
// get ip address from proxy string
re := regexp.MustCompile(`(?m)\/\/(.*?)(:\d+)?$`)
match := re.FindStringSubmatch(proxy)

if len(match) < 2 {
return "", fmt.Errorf("invalid proxy address")
}

addr = match[1]
} else if gateway, _, err := GetDefaultRoute(); err == nil {
addr = gateway.String()
}

// Construct Clash API URL
url := fmt.Sprintf("http://%s:9090", addr)

// Check if Clash API is reachable
resp, err := http.Get(url)

if err != nil {
return "", err
}

// Check response
var clashAPIRootResponse ClashAPIRootResponse
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&clashAPIRootResponse)

if err != nil {
return "", err
}

if clashAPIRootResponse.Hello == "clash" {
return url, nil
}

if clashAPIRootResponse.Message == "Unauthorized" {
return url, nil
}

return "", fmt.Errorf("clash api not detected")
}

// func GetClashApiVersion() (string, error) {
// url, err := getClashApiBaseUrl()

// if err != nil {
// return "", err
// }

// resp, err := http.Get(fmt.Sprintf("%s/version", url))

// if err != nil {
// return "", err
// }

// defer resp.Body.Close()

// // {"premium":true,"version":"2023.08.17-11-g0f901d0"}
// bodyBytes, err := io.ReadAll(resp.Body)

// if err != nil {
// return "", err
// }

// body := string(bodyBytes)
// re := regexp.MustCompile(`"version":"(.*?)"`)
// match := re.FindStringSubmatch(body)

// if len(match) < 2 {
// return "", fmt.Errorf("clash version not found in api response")
// }

// return match[1], nil
// }
37 changes: 37 additions & 0 deletions checks/layer3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package checks

import (
"fmt"
"time"

"github.com/go-ping/ping"
)

func CheckReachabilityWithICMP(host string) (string, error) {
pinger, err := ping.NewPinger(host)
if err != nil {
return "", err
}

pinger.Count = 3
pinger.Debug = true
pinger.Interval = 200 * time.Millisecond
pinger.Timeout = 2 * time.Second

err = pinger.Run()

if err != nil {
return "", err
}

stats := pinger.Statistics()

statsString := fmt.Sprintf(
"%s/%s/%s",
stats.MinRtt.Round(time.Millisecond),
stats.AvgRtt.Round(time.Millisecond),
stats.MaxRtt.Round(time.Millisecond),
)

return statsString, nil
}
65 changes: 65 additions & 0 deletions checks/layer7.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package checks

import (
"fmt"
"io"
"net"
"net/http"
"regexp"
"time"

"github.com/miekg/dns"
)

func CheckNameserverAvailability(s string) error {
c := new(dns.Client)
c.Dialer = &net.Dialer{
Timeout: 3 * time.Second,
}
m := new(dns.Msg)
m.SetQuestion("apple.com.", dns.TypeA)
_, _, err := c.Exchange(m, s)

if err != nil {
return err
}

// fmt.Printf("%v %v", in, rtt)
return nil
}

func CheckCaptivePortal() error {
resp, err := http.Get("http://connectivitycheck.gstatic.com/generate_204")

if err != nil || resp.StatusCode != 204 {
return err
}

return nil
}

func GetCloudflareEdgeTrace() (string, error) {
resp, err := http.Get("https://www.cloudflare.com/cdn-cgi/trace")

if err != nil {
return "", err
}

defer resp.Body.Close()

bodyBytes, err := io.ReadAll(resp.Body)

if err != nil {
return "", err
}

body := string(bodyBytes)
re := regexp.MustCompile(`loc=(.*?)\n`)
match := re.FindStringSubmatch(body)

if len(match) < 2 {
return "", fmt.Errorf("could not determine edge pop")
}

return match[1], nil
}
67 changes: 67 additions & 0 deletions checks/system.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package checks

import (
"fmt"
"net"
"os"
"regexp"

"github.com/jackpal/gateway"
)

func getSystemDefaultProxy() (string, error) {
http_proxy := os.Getenv("http_proxy")
https_proxy := os.Getenv("https_proxy")
all_proxy := os.Getenv("all_proxy")

if all_proxy != "" {
return all_proxy, nil
}

if http_proxy != "" {
return http_proxy, nil
}

if https_proxy != "" {
return https_proxy, nil
}

return "", fmt.Errorf("no proxy detected")
}

func GetDefaultRoute() (net.IP, string, error) {
// check default gateway
gw, err := gateway.DiscoverGateway()

if err != nil {
return nil, "", fmt.Errorf("error reading default route: %s", err)
}

stats, err := CheckReachabilityWithICMP(gw.String())

if err != nil {
return gw, stats, fmt.Errorf("default route is unreachable: %s", err)
}

return gw, stats, nil
}

func GetDefaultNameserver() (string, error) {
// get default ns from /etc/resolv.conf
byteString, err := os.ReadFile("/etc/resolv.conf")

if err != nil {
return "", err
}

s := string(byteString)

re := regexp.MustCompile(`(?m)^nameserver( *|\t*)(.*?)$`)
match := re.FindStringSubmatch(s)

if len(match) < 2 {
return "", fmt.Errorf("match is less than 2")
}

return match[2], nil
}
68 changes: 68 additions & 0 deletions checks/tailscale.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package checks

import (
"context"
"fmt"
"time"

"tailscale.com/client/tailscale"
"tailscale.com/types/key"
"tailscale.com/ipn/ipnstate"
)

// func findTailscalePeerByStableNodeID(peers map[key.NodePublic]*ipnstate.PeerStatus, id tailcfg.StableNodeID) *ipnstate.PeerStatus {
// for _, p := range peers {
// if p.ID == id {
// return p
// }
// }

// return nil
// }

func FindActiveExitNodeFromPeersMap(peers map[key.NodePublic]*ipnstate.PeerStatus) *ipnstate.PeerStatus {
for _, p := range peers {
if p.ExitNode {
return p
}
}

return nil
}

func GetTailscaleStatus() string {
// check tailscale status
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
ts, err := tailscale.Status(ctx) // https://pkg.go.dev/tailscale.com@v1.40.0/ipn/ipnstate#Status
defer cancel()

if err != nil {
return fmt.Sprintf("[!] Could not determine tailscaled status: %s", err)
}

// https://github.com/tailscale/tailscale/blob/9bdaece3d7c3c83aae01e0736ba54e833f4aea51/cmd/tailscale/cli/status.go#L162-L196

if !ts.Self.Online {
return fmt.Sprintf("[~] We're offline on tsnet: BackendState=%s", ts.BackendState)
}

exitNodeStatus := FindActiveExitNodeFromPeersMap(ts.Peer)

if exitNodeStatus == nil {
return "[+] We're online on tsnet; not using any exit node"
}

if exitNodeStatus.Active {
if exitNodeStatus.Relay != "" && exitNodeStatus.CurAddr == "" {
return fmt.Sprintf("[~] We're online on tsnet; exit node \"%s\" via relay %s", exitNodeStatus.HostName, exitNodeStatus.Relay)
}

if exitNodeStatus.CurAddr != "" {
return fmt.Sprintf("[+] We're online on tsnet; exit node \"%s\" via %s", exitNodeStatus.HostName, exitNodeStatus.CurAddr)
}

return fmt.Sprintf("[!] We're online on tsnet; exit node \"%s\" (unknown connection)", exitNodeStatus.HostName)
}

return fmt.Sprintf("[+] We're online on tsnet; exit node \"%s\" is inactive", exitNodeStatus.HostName)
}
Loading

0 comments on commit 92a79cb

Please sign in to comment.