diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml new file mode 100644 index 0000000..04d09f2 --- /dev/null +++ b/.github/workflows/build-publish.yml @@ -0,0 +1,34 @@ +name: Build Docker images + +on: + push: + branches: + - main + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set outputs + id: vars + run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/xray-checker:latest + ${{ secrets.DOCKERHUB_USERNAME }}/xray-checker:${{ steps.vars.outputs.sha_short }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80f0a7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +all/ +configs/* +build/ +xray-checker \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1d72d67 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.21 as builder + +ARG TARGETPLATFORM +ARG BUILDPLATFORM +ARG TARGETOS +ARG TARGETARCH +ARG GIT_TAG +ARG GIT_COMMIT + +ENV CGO_ENABLED=0 +ENV GO111MODULE=on + +WORKDIR /go/src/github.com/kutovoys/xray-checker + +# Cache the download before continuing +COPY go.mod go.mod +COPY go.sum go.sum +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=${CGO_ENABLED} GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -a -installsuffix cgo -o /usr/bin/xray-checker . + +FROM --platform=${BUILDPLATFORM:-linux/amd64} teddysun/xray:1.8.23 + +LABEL org.opencontainers.image.source=https://github.com/kutovoys/xray-checker + +WORKDIR / +COPY --from=builder /usr/bin/xray-checker /checker/xray-checker +# USER nonroot:nonroot + +CMD ["/checker/xray-checker"] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..314df8a --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module xray-checker + +go 1.21.5 + +require ( + github.com/go-co-op/gocron v1.37.0 + golang.org/x/net v0.28.0 +) + +require ( + github.com/google/uuid v1.4.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + go.uber.org/atomic v1.9.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a0452ed --- /dev/null +++ b/go.sum @@ -0,0 +1,40 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= +github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ba7c019 --- /dev/null +++ b/main.go @@ -0,0 +1,222 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/go-co-op/gocron" + "golang.org/x/net/proxy" +) + +type Config struct { + Log map[string]interface{} `json:"log"` + Inbounds []struct { + Listen string `json:"listen"` + Port int `json:"port"` + Protocol string `json:"protocol"` + Sniffing struct { + Enabled bool `json:"enabled"` + DestOverride []string `json:"destOverride"` + RouteOnly bool `json:"routeOnly"` + } `json:"sniffing"` + } `json:"inbounds"` + Outbounds []map[string]interface{} `json:"outbounds"` + Webhook string `json:"webhook"` +} + +type LogData struct { + ConfigFile string + SourceIP string + VPNIP string + WebhookURL string + ProxyAddress string + Status string + Error error +} + +func getIP(url string, client *http.Client) (string, error) { + resp, err := client.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + ip, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return strings.TrimSpace(string(ip)), nil +} + +func runXray(configPath string) (*exec.Cmd, error) { + cmd := exec.Command("xray", "-c", configPath) + err := cmd.Start() + if err != nil { + return nil, err + } + return cmd, nil +} + +func killXray(cmd *exec.Cmd) error { + return cmd.Process.Kill() +} + +func createProxyClient(proxyAddress string) (*http.Client, error) { + proxyURL, err := url.Parse(proxyAddress) + if err != nil { + return nil, fmt.Errorf("неправильный формат прокси: %v", err) + } + + dialer, err := proxy.FromURL(proxyURL, proxy.Direct) + if err != nil { + return nil, fmt.Errorf("ошибка создания прокси-диалера: %v", err) + } + + transport := &http.Transport{ + Dial: dialer.Dial, + } + + client := &http.Client{ + Transport: transport, + } + + return client, nil +} + +func processConfigFile(configPath string) { + logData := LogData{ConfigFile: configPath} + + configData, err := os.ReadFile(configPath) + if err != nil { + logData.Error = fmt.Errorf("ошибка чтения конфигурационного файла: %v", err) + logResult(logData) + return + } + + var config Config + err = json.Unmarshal(configData, &config) + if err != nil { + logData.Error = fmt.Errorf("ошибка парсинга конфигурационного файла: %v", err) + logResult(logData) + return + } + + logData.WebhookURL = config.Webhook + if logData.WebhookURL == "" { + logData.Error = fmt.Errorf("webhook URL не найден в конфигурационном файле") + logResult(logData) + return + } + + ipCheckURL := "https://ifconfig.io" + logData.SourceIP, err = getIP(ipCheckURL, &http.Client{}) + if err != nil { + logData.Error = fmt.Errorf("ошибка получения исходного IP: %v", err) + logResult(logData) + return + } + + listen := config.Inbounds[0].Listen + port := config.Inbounds[0].Port + logData.ProxyAddress = fmt.Sprintf("socks5://%s:%d", listen, port) + + cmd, err := runXray(configPath) + if err != nil { + logData.Error = fmt.Errorf("ошибка запуска Xray: %v", err) + logResult(logData) + return + } + defer killXray(cmd) + time.Sleep(2 * time.Second) + + proxyClient, err := createProxyClient(logData.ProxyAddress) + if err != nil { + logData.Error = fmt.Errorf("ошибка создания прокси-клиента: %v", err) + logResult(logData) + return + } + + logData.VPNIP, err = getIP(ipCheckURL, proxyClient) + if err != nil { + logData.Error = fmt.Errorf("ошибка получения VPN IP через прокси: %v", err) + logResult(logData) + return + } + + if logData.VPNIP != logData.SourceIP { + _, err = http.Get(logData.WebhookURL) + if err != nil { + logData.Error = fmt.Errorf("ошибка отправки статуса: %v", err) + logData.Status = "Не удалось отправить статус" + } else { + logData.Status = "Статус отправлен успешно" + } + } else { + logData.Status = "IP-адреса совпадают, статус не отправлен" + } + + logResult(logData) +} + +func logResult(logData LogData) { + var logMsg string + + if logData.Error != nil { + logMsg = fmt.Sprintf("Error: %v | Config: %s | Source IP: %s | VPN IP: %s", + logData.Error, logData.ConfigFile, logData.SourceIP, logData.VPNIP) + } else { + logMsg = fmt.Sprintf("Status: %s | Config: %s | Source IP: %s | VPN IP: %s", + logData.Status, logData.ConfigFile, logData.SourceIP, logData.VPNIP) + } + + log.Println(logMsg) +} + +func scheduleConfigs(configDir string, scheduler *gocron.Scheduler) { + files, err := os.ReadDir(configDir) + if err != nil { + fmt.Println("Ошибка чтения директории:", err) + return + } + + for _, file := range files { + if filepath.Ext(file.Name()) == ".json" { + configPath := filepath.Join(configDir, file.Name()) + // Запускаем задачу в шедулере каждые 10 секунд + scheduler.Every(40).Seconds().Do(func() { + processConfigFile(configPath) + }) + } + } +} + +func main() { + configDir := "./configs" // директория с конфигурационными файлами + var wg sync.WaitGroup + + // Создаем новый шедулер + scheduler := gocron.NewScheduler(time.UTC) + + // Планируем обработку конфигурационных файлов + scheduleConfigs(configDir, scheduler) + + // Запускаем шедулер в отдельной горутине + wg.Add(1) + go func() { + defer wg.Done() + scheduler.StartBlocking() + }() + + // Ожидаем завершения работы + wg.Wait() +}