131 lines
3.1 KiB
Go
131 lines
3.1 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bufio"
|
||
|
|
"context"
|
||
|
|
"flag"
|
||
|
|
"fmt"
|
||
|
|
"log"
|
||
|
|
"net"
|
||
|
|
"net/http"
|
||
|
|
"net/netip"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/go-gost/plugin/bypass/proto"
|
||
|
|
"google.golang.org/grpc"
|
||
|
|
)
|
||
|
|
|
||
|
|
type server struct {
|
||
|
|
proto.UnimplementedBypassServer
|
||
|
|
mu sync.RWMutex
|
||
|
|
list []netip.Prefix
|
||
|
|
lookup func(context.Context, string, string) ([]netip.Addr, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *server) Bypass(ctx context.Context, in *proto.BypassRequest) (*proto.BypassReply, error) {
|
||
|
|
// GOST 服务级插件返回 Ok=true 表示拒绝,外层 whitelist 参数不生效。
|
||
|
|
reply := &proto.BypassReply{Ok: true}
|
||
|
|
host := in.GetAddr()
|
||
|
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
||
|
|
host = h
|
||
|
|
}
|
||
|
|
var ips []netip.Addr
|
||
|
|
if ip, err := netip.ParseAddr(host); err == nil {
|
||
|
|
ips = []netip.Addr{ip}
|
||
|
|
} else {
|
||
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
var err error
|
||
|
|
ips, err = s.lookup(ctx, "ip", host)
|
||
|
|
if err != nil || len(ips) == 0 {
|
||
|
|
return reply, nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
s.mu.RLock()
|
||
|
|
defer s.mu.RUnlock()
|
||
|
|
for _, ip := range ips {
|
||
|
|
allowed := false
|
||
|
|
for _, prefix := range s.list {
|
||
|
|
if prefix.Contains(ip.Unmap()) {
|
||
|
|
allowed = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 任一解析结果不在白名单内就拒绝,避免混合地址被放行。
|
||
|
|
if !allowed {
|
||
|
|
return reply, nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
reply.Ok = false
|
||
|
|
return reply, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *server) refresh(url string) error {
|
||
|
|
client := http.Client{Timeout: 30 * time.Second}
|
||
|
|
resp, err := client.Get(url)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
if resp.StatusCode != http.StatusOK {
|
||
|
|
return fmt.Errorf("list returned HTTP %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
var list []netip.Prefix
|
||
|
|
scanner := bufio.NewScanner(resp.Body)
|
||
|
|
for scanner.Scan() {
|
||
|
|
line, _, _ := strings.Cut(scanner.Text(), "#")
|
||
|
|
line = strings.TrimSpace(line)
|
||
|
|
if line == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
prefix, err := netip.ParsePrefix(line)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("invalid CIDR %q: %w", line, err)
|
||
|
|
}
|
||
|
|
list = append(list, prefix.Masked())
|
||
|
|
}
|
||
|
|
if err := scanner.Err(); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if len(list) == 0 {
|
||
|
|
return fmt.Errorf("empty CIDR list")
|
||
|
|
}
|
||
|
|
// 完整加载成功才替换,下载或解析失败继续使用旧列表。
|
||
|
|
s.mu.Lock()
|
||
|
|
s.list = list
|
||
|
|
s.mu.Unlock()
|
||
|
|
log.Printf("loaded %d CIDR rules", len(list))
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
addr := flag.String("listen-addr", "127.0.0.1:18080", "gRPC listen address")
|
||
|
|
url := flag.String("list-url", "https://china-operator-ip.yfgao.com/china.txt", "CIDR list URL")
|
||
|
|
period := flag.Duration("refresh", 24*time.Hour, "CIDR refresh interval")
|
||
|
|
flag.Parse()
|
||
|
|
if *period <= 0 {
|
||
|
|
log.Fatal("refresh must be positive")
|
||
|
|
}
|
||
|
|
s := &server{lookup: net.DefaultResolver.LookupNetIP}
|
||
|
|
if err := s.refresh(*url); err != nil {
|
||
|
|
log.Fatal(err)
|
||
|
|
}
|
||
|
|
go func() {
|
||
|
|
for range time.Tick(*period) {
|
||
|
|
if err := s.refresh(*url); err != nil {
|
||
|
|
log.Printf("refresh failed, keeping previous list: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
listener, err := net.Listen("tcp", *addr)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatal(err)
|
||
|
|
}
|
||
|
|
rpc := grpc.NewServer()
|
||
|
|
proto.RegisterBypassServer(rpc, s)
|
||
|
|
log.Printf("listening on %s", *addr)
|
||
|
|
log.Fatal(rpc.Serve(listener))
|
||
|
|
}
|