Add simple proxy portal

This commit is contained in:
世界 2022-04-30 10:31:12 +08:00
parent 5ca1ed97ef
commit c66f869581
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
6 changed files with 691 additions and 0 deletions

View file

@ -0,0 +1,287 @@
package main
import (
"context"
"encoding/json"
"github.com/go-resty/resty/v2"
E "github.com/sagernet/sing/common/exceptions"
)
type NodeClient struct {
client *resty.Client
}
func NewNodeClient(baseURL string, token string, node string) *NodeClient {
r := resty.New()
r.SetBaseURL(baseURL)
r.SetQueryParams(map[string]string{
"node_id": node,
"token": token,
})
// r.SetDebug(true)
return &NodeClient{r}
}
type ShadowsocksUserList struct {
Port uint16
Method string
Users map[int]string // id password
}
type RawShadowsocksUserList struct {
Data []RawShadowsocksUser `json:"data"`
}
type RawShadowsocksUser struct {
Id int `json:"id"`
Port uint16 `json:"port"`
Cipher string `json:"cipher"`
Secret string `json:"secret"`
}
func (c *NodeClient) GetShadowsocksUserList(ctx context.Context) (*ShadowsocksUserList, error) {
resp, err := c.client.R().
SetContext(ctx).
SetResult(new(RawShadowsocksUserList)).
Get("/api/v1/server/ShadowsocksTidalab/user")
if err != nil {
return nil, err
}
if !resp.IsSuccess() {
return nil, E.New("HTTP ", resp.StatusCode(), " ", resp.Body())
}
rawUserList := resp.Result().(*RawShadowsocksUserList)
userList := &ShadowsocksUserList{
Method: rawUserList.Data[0].Cipher,
Port: rawUserList.Data[0].Port,
Users: make(map[int]string),
}
for _, item := range rawUserList.Data {
if item.Cipher != userList.Method {
return nil, E.New("not unique method in item ", item.Id)
}
if item.Port != userList.Port {
return nil, E.New("not unique port in item ", item.Id)
}
userList.Users[item.Id] = item.Secret
}
return userList, nil
}
type TrojanUserList struct {
Users map[int]string // id password
}
type RawTrojanUserList struct {
Msg string `json:"msg"`
Data []RawTrojanUser `json:"data"`
}
type RawTrojanUser struct {
ID int `json:"id"`
T int `json:"t"`
U int64 `json:"u"`
D int64 `json:"d"`
TransferEnable int64 `json:"transfer_enable"`
TrojanUser struct {
Password string `json:"password"`
} `json:"trojan_user"`
}
func (c *NodeClient) GetTrojanUserList(ctx context.Context) (*TrojanUserList, error) {
resp, err := c.client.R().
SetContext(ctx).
SetResult(new(RawTrojanUserList)).
Get("/api/v1/server/TrojanTidalab/user")
if err != nil {
return nil, err
}
if !resp.IsSuccess() {
return nil, E.New("HTTP ", resp.StatusCode(), " ", resp.String())
}
rawUserList := resp.Result().(*RawTrojanUserList)
userList := &TrojanUserList{
Users: make(map[int]string),
}
for _, item := range rawUserList.Data {
userList.Users[item.ID] = item.TrojanUser.Password
}
return userList, nil
}
type TrojanConfig struct {
LocalPort uint16
SNI string
}
type RawTrojanConfig struct {
LocalPort uint16 `json:"local_port"`
Ssl struct {
Sni string `json:"sni"`
} `json:"ssl"`
}
func (c *NodeClient) GetTrojanConfig(ctx context.Context) (*TrojanConfig, error) {
resp, err := c.client.R().
SetContext(ctx).
SetQueryParam("local_port", "1").
Get("/api/v1/server/TrojanTidalab/config")
if err != nil {
return nil, err
}
if !resp.IsSuccess() {
return nil, E.New("HTTP ", resp.StatusCode(), " ", resp.String())
}
rawConfig := new(RawTrojanConfig)
err = json.Unmarshal(resp.Body(), rawConfig)
if err != nil {
return nil, E.Cause(err, "parse raw trojan config")
}
trojanConfig := new(TrojanConfig)
trojanConfig.LocalPort = rawConfig.LocalPort
trojanConfig.SNI = rawConfig.Ssl.Sni
return trojanConfig, nil
}
type VMessUserList struct {
AlterID int
Users map[int]string // id uuid
}
type RawVMessUserList struct {
Msg string `json:"msg"`
Data []RawVMessUser `json:"data"`
}
type RawVMessUser struct {
Id int `json:"id"`
T int `json:"t"`
U int64 `json:"u"`
D int64 `json:"d"`
TransferEnable int64 `json:"transfer_enable"`
V2RayUser struct {
Uuid string `json:"uuid"`
Email string `json:"email"`
AlterId int `json:"alter_id"`
Level int `json:"level"`
} `json:"v2ray_user"`
}
func (c *NodeClient) GetVMessUserList(ctx context.Context) (*VMessUserList, error) {
resp, err := c.client.R().
SetContext(ctx).
SetResult(new(RawVMessUserList)).
Get("/api/v1/server/Deepbwork/user")
if err != nil {
return nil, err
}
if !resp.IsSuccess() {
return nil, E.New("HTTP ", resp.StatusCode(), " ", resp.String())
}
rawUserList := resp.Result().(*RawVMessUserList)
userList := &VMessUserList{
AlterID: rawUserList.Data[0].V2RayUser.AlterId,
Users: make(map[int]string),
}
for _, user := range rawUserList.Data {
userList.Users[user.Id] = user.V2RayUser.Uuid
}
return userList, nil
}
type VMessConfig struct {
Port uint16
Network string
Security string
}
type RawV2RayConfig struct {
Inbounds []struct {
Protocol string `json:"protocol"`
Port uint16 `json:"port"`
StreamSettings struct {
Network string `json:"network"`
Security string `json:"security,omitempty"`
} `json:"streamSettings,omitempty"`
} `json:"inbounds"`
}
func (c *NodeClient) GetVMessConfig(ctx context.Context) (*VMessConfig, error) {
resp, err := c.client.R().
SetContext(ctx).
SetQueryParam("local_port", "1").
Get("/api/v1/server/Deepbwork/config")
if err != nil {
return nil, err
}
if !resp.IsSuccess() {
return nil, E.New("HTTP ", resp.StatusCode(), " ", resp.String())
}
rawConfig := new(RawV2RayConfig)
err = json.Unmarshal(resp.Body(), rawConfig)
if err != nil {
return nil, err
}
vmessConfig := new(VMessConfig)
vmessConfig.Port = rawConfig.Inbounds[0].Port
vmessConfig.Network = rawConfig.Inbounds[0].StreamSettings.Network
vmessConfig.Security = rawConfig.Inbounds[0].StreamSettings.Security
return vmessConfig, nil
}
type UserTraffic struct {
UID int `json:"user_id"`
Upload int64 `json:"u"`
Download int64 `json:"d"`
}
func (c *NodeClient) ReportShadowsocksTraffic(ctx context.Context, userTraffic []UserTraffic) error {
return c.reportTraffic(ctx, "/api/v1/server/ShadowsocksTidalab/submit", userTraffic)
}
func (c *NodeClient) ReportVMessTraffic(ctx context.Context, userTraffic []UserTraffic) error {
return c.reportTraffic(ctx, "/api/v1/server/Deepbwork/submit", userTraffic)
}
func (c *NodeClient) ReportTrojanTraffic(ctx context.Context, userTraffic []UserTraffic) error {
return c.reportTraffic(ctx, "/api/v1/server/TrojanTidalab/submit", userTraffic)
}
func (c *NodeClient) reportTraffic(ctx context.Context, path string, userTraffic []UserTraffic) error {
resp, err := c.client.R().
SetContext(ctx).
SetBody(userTraffic).
Post(path)
if err != nil {
return err
}
if !resp.IsSuccess() {
return E.New("HTTP ", resp.StatusCode(), " ", resp.String())
}
return nil
}

View file

@ -0,0 +1,24 @@
package main
import "encoding/json"
type Config struct {
URL string `json:"url"`
Token string `json:"token"`
Nodes []Node `json:"nodes"`
TLS *TLSConfig `json:"tls,omitempty"`
Debug bool `json:"debug"`
}
type Node struct {
ID int `json:"id"`
Type string `json:"type"`
Domain string `json:"domain"`
}
type TLSConfig struct {
Insecure bool `json:"insecure"`
Email string `json:"email"`
DNSProvider string `json:"dns_provider"`
DNSEnv json.RawMessage `json:"dns_env"`
}

View file

@ -0,0 +1,237 @@
package main
import (
"context"
"crypto/tls"
"encoding/json"
"github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
"io/ioutil"
"net"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/sagernet/sing"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/random"
"github.com/sagernet/sing/protocol/socks"
"github.com/sagernet/sing/protocol/trojan"
transTLS "github.com/sagernet/sing/transport/tls"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var configPath string
func main() {
command := &cobra.Command{
Use: "portal-v2board [-c config.json]",
Args: cobra.NoArgs,
Version: sing.VersionStr,
Run: run,
}
command.Flags().StringVarP(&configPath, "config", "c", "config.json", "set config path")
if err := command.Execute(); err != nil {
logrus.Fatal(err)
}
}
func run(cmd *cobra.Command, args []string) {
data, err := ioutil.ReadFile(configPath)
if err != nil {
logrus.Fatal(E.Cause(err, "read config"))
}
config := new(Config)
err = json.Unmarshal(data, config)
if err != nil {
logrus.Fatal(E.Cause(err, "parse config"))
}
if config.Debug {
logrus.SetLevel(logrus.TraceLevel)
}
if len(config.Nodes) == 0 {
logrus.Fatal("empty nodes")
}
var instances []Instance
for _, node := range config.Nodes {
client := NewNodeClient(config.URL, config.Token, strconv.Itoa(node.ID))
switch node.Type {
case "trojan":
instances = append(instances, NewTrojanInstance(client, node))
default:
logrus.Fatal("unsupported node type ", node.Type, " (id: ", node.ID, ")")
}
}
for _, instance := range instances {
err = instance.Start()
if err != nil {
logrus.Fatal(err)
}
}
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)
<-osSignals
for _, instance := range instances {
instance.Close()
}
}
type Instance interface {
Start() error
Close() error
}
type TrojanInstance struct {
*NodeClient
id int
domain string
listener net.Listener
service trojan.Service[int]
user UserManager
reloadTicker *time.Ticker
}
func NewTrojanInstance(client *NodeClient, node Node) *TrojanInstance {
t := &TrojanInstance{
NodeClient: client,
id: node.ID,
domain: node.Domain,
user: NewUserManager(),
}
t.service = trojan.NewService[int](t)
return t
}
func (i *TrojanInstance) Start() error {
err := i.reloadUsers()
if err != nil {
return err
}
trojanConfig, err := i.GetTrojanConfig(context.Background())
if err != nil {
return E.CauseF(err, i.id, ": read trojan config")
}
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{
Port: int(trojanConfig.LocalPort),
})
if err != nil {
return E.CauseF(err, i.id, ": listen at tcp:", trojanConfig.LocalPort, ", check server configuration!")
}
i.listener = tls.NewListener(tcpListener, &tls.Config{
Rand: random.Blake3KeyedHash(),
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return transTLS.GenerateCertificate(info.ServerName)
},
})
logrus.Info(i.id, ": started at ", tcpListener.Addr())
go i.loopRequests()
i.reloadTicker = time.NewTicker(time.Minute)
go i.loopReload()
return nil
}
func (i *TrojanInstance) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
userCtx := ctx.(*trojan.Context[int])
conn = i.user.TrackConnection(userCtx.User, conn)
logrus.Info(i.id, ": user ", userCtx.User, " TCP ", metadata.Source, " ==> ", metadata.Destination)
destConn, err := network.SystemDialer.DialContext(context.Background(), "tcp", metadata.Destination)
if err != nil {
return err
}
return rw.CopyConn(ctx, conn, destConn)
}
func (i *TrojanInstance) NewPacketConnection(ctx context.Context, conn socks.PacketConn, metadata M.Metadata) error {
userCtx := ctx.(*trojan.Context[int])
conn = i.user.TrackPacketConnection(userCtx.User, conn)
logrus.Info(i.id, ": user ", userCtx.User, " UDP ", metadata.Source, " ==> ", metadata.Destination)
udpConn, err := net.ListenUDP("udp", nil)
if err != nil {
return err
}
return socks.CopyNetPacketConn(ctx, udpConn, conn)
}
func (i *TrojanInstance) loopRequests() {
for {
conn, err := i.listener.Accept()
if err != nil {
logrus.Debug(E.CauseF(err, i.id, ": listener exited"))
return
}
go func() {
hErr := i.service.NewConnection(context.Background(), conn, M.Metadata{
Protocol: "tls",
Source: M.AddrPortFromNetAddr(conn.RemoteAddr()),
})
if hErr != nil {
i.HandleError(hErr)
}
}()
}
}
func (i *TrojanInstance) loopReload() {
for range i.reloadTicker.C {
err := i.reloadUsers()
if err != nil {
i.HandleError(E.CauseF(err, "reload user"))
}
traffics := i.user.ReadTraffics()
if len(traffics) > 0 {
err = i.ReportTrojanTraffic(context.Background(), traffics)
if err != nil {
i.HandleError(E.CauseF(err, "report traffic"))
}
}
}
}
func (i *TrojanInstance) reloadUsers() error {
logrus.Debug(i.id, ": fetching users...")
userList, err := i.GetTrojanUserList(context.Background())
if err != nil {
return E.CauseF(err, i.id, ": get user list")
}
if len(userList.Users) == 0 {
logrus.Warn(i.id, ": empty users")
}
i.service.ResetUsers()
for id, password := range userList.Users {
err = i.service.AddUser(id, password)
if err != nil {
logrus.Warn(E.CauseF(err, i.id, ": add user"))
}
}
logrus.Debug(i.id, ": loaded ", len(userList.Users), " users")
return nil
}
func (i *TrojanInstance) HandleError(err error) {
common.Close(err)
if E.IsClosed(err) {
return
}
logrus.Warn(i.id, ": ", err)
}
func (i *TrojanInstance) Close() error {
i.reloadTicker.Stop()
return i.listener.Close()
}

View file

@ -0,0 +1,134 @@
package main
import (
"io"
"net"
"sync"
"sync/atomic"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/protocol/socks"
)
type UserManager struct {
access sync.Mutex
users map[int]*User
}
type User struct {
Upload uint64
Download uint64
}
func NewUserManager() UserManager {
return UserManager{
users: make(map[int]*User),
}
}
func (m *UserManager) TrackConnection(userId int, conn net.Conn) net.Conn {
m.access.Lock()
defer m.access.Unlock()
var user *User
if u, loaded := m.users[userId]; loaded {
user = u
} else {
user = new(User)
m.users[userId] = user
}
return &TrackConn{conn, user}
}
func (m *UserManager) TrackPacketConnection(userId int, conn socks.PacketConn) socks.PacketConn {
m.access.Lock()
defer m.access.Unlock()
var user *User
if u, loaded := m.users[userId]; loaded {
user = u
} else {
user = new(User)
m.users[userId] = user
}
return &TrackPacketConn{conn, user}
}
func (m *UserManager) ReadTraffics() []UserTraffic {
m.access.Lock()
defer m.access.Unlock()
traffic := make([]UserTraffic, 0, len(m.users))
for userId, user := range m.users {
upload := atomic.SwapUint64(&user.Upload, 0)
download := atomic.SwapUint64(&user.Download, 0)
if upload == 0 && download == 0 {
continue
}
traffic = append(traffic, UserTraffic{
UID: userId,
Upload: int64(upload),
Download: int64(download),
})
}
return traffic
}
type TrackConn struct {
net.Conn
*User
}
func (c *TrackConn) Read(p []byte) (n int, err error) {
n, err = c.Conn.Read(p)
if n > 0 {
atomic.AddUint64(&c.Upload, uint64(n))
}
return
}
func (c *TrackConn) Write(p []byte) (n int, err error) {
n, err = c.Conn.Write(p)
if n > 0 {
atomic.AddUint64(&c.Download, uint64(n))
}
return
}
func (c *TrackConn) WriteTo(w io.Writer) (n int64, err error) {
n, err = io.Copy(w, c.Conn)
if n > 0 {
atomic.AddUint64(&c.Upload, uint64(n))
}
return
}
func (c *TrackConn) ReadFrom(r io.Reader) (n int64, err error) {
n, err = io.Copy(c.Conn, r)
if n > 0 {
atomic.AddUint64(&c.Download, uint64(n))
}
return
}
type TrackPacketConn struct {
socks.PacketConn
*User
}
func (c *TrackPacketConn) ReadPacket(buffer *buf.Buffer) (*M.AddrPort, error) {
destination, err := c.PacketConn.ReadPacket(buffer)
if err == nil {
atomic.AddUint64(&c.Upload, uint64(buffer.Len()))
}
return destination, err
}
func (c *TrackPacketConn) WritePacket(buffer *buf.Buffer, destination *M.AddrPort) error {
n := buffer.Len()
err := c.PacketConn.WritePacket(buffer, destination)
if err == nil {
atomic.AddUint64(&c.Download, uint64(n))
}
return err
}

1
go.mod
View file

@ -4,6 +4,7 @@ go 1.18
require (
github.com/cloudflare/cloudflare-go v0.38.0
github.com/go-resty/resty/v2 v2.7.0
github.com/openacid/low v0.1.21
github.com/oschwald/geoip2-golang v1.7.0
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb

8
go.sum
View file

@ -9,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
@ -73,17 +75,23 @@ github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695AP
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d h1:q4JksJ2n0fmbXC0Aj0eOs6E0AcPqnKglxWXWFqGD6x0=