Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,8 @@ complete -c gg -x -a "(__fish_complete_gg_subcommand)"

### Protocol

- [x] Hysteria2 (HY2)
- [x] AnyTLS
- [x] HTTP(S)
- [x] Socks
- [x] Socks4
Expand Down
2 changes: 2 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ complete -c gg -x -a "(__fish_complete_gg_subcommand)"

### 协议类型

- [x] Hysteria2 (HY2)
- [x] AnyTLS
- [x] HTTP(S)
- [x] Socks5
- [x] VMess(AEAD, alterID=0) / VLESS
Expand Down
16 changes: 13 additions & 3 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,7 @@ $ gg git clone https://github.com/mzz2017/gg.git`)
} else {
noUDP = v.GetBool("no_udp")
}
if !noUDP && !dialer.SupportUDP() {
log.Info("Your proxy server does not support UDP, so we will not redirect UDP traffic.")
}
logUDPRedirectLimitation(log, noUDP, dialer.SupportUDP())
// Get proxy_private from argument first, then from configuration file.
var proxyPrivate bool
proxyPrivateFlag := cmd.Flags().Lookup("proxyprivate")
Expand Down Expand Up @@ -159,9 +157,21 @@ func init() {
rootCmd.PersistentFlags().Bool("proxyprivate", false, "redirect traffic to private address")
rootCmd.PersistentFlags().String("testnode", "true", "test the connectivity before connecting to the node")
rootCmd.PersistentFlags().Bool("select", false, "manually select the node to connect from the subscription")
configureCommandPassthrough(rootCmd)
rootCmd.AddCommand(configCmd)
}

func configureCommandPassthrough(command *cobra.Command) {
command.Args = cobra.ArbitraryArgs
command.Flags().SetInterspersed(false)
}

func logUDPRedirectLimitation(log *logrus.Logger, noUDP, supportUDP bool) {
if !noUDP && !supportUDP {
log.Info("UDP redirection is disabled or unsupported for this node; DNS traffic may still be handled.")
}
}

func NewLogger(verbose int) *logrus.Logger {
log := logrus.New()

Expand Down
68 changes: 68 additions & 0 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cmd

import (
"bytes"
"reflect"
"strings"
"testing"

"github.com/spf13/cobra"
)

func TestLogUDPRedirectLimitation(t *testing.T) {
tests := []struct {
name string
noUDP bool
supportUDP bool
wantLog bool
}{
{name: "node disabled", supportUDP: false, wantLog: true},
{name: "node supports UDP", supportUDP: true},
{name: "globally disabled", noUDP: true, supportUDP: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var output bytes.Buffer
log := NewLogger(1)
log.SetOutput(&output)

logUDPRedirectLimitation(log, test.noUDP, test.supportUDP)

got := output.String()
if test.wantLog != (got != "") {
t.Fatalf("log output = %q, wantLog = %v", got, test.wantLog)
}
if test.wantLog && (!strings.Contains(got, "disabled or unsupported") || !strings.Contains(got, "DNS traffic may still be handled")) {
t.Fatalf("log output = %q", got)
}
})
}
}

func TestCommandArgumentsPassThroughUnchanged(t *testing.T) {
var (
gotArgs []string
gotVerbose bool
)
command := &cobra.Command{
Use: "gg [flags] [command [argument ...]]",
Run: func(command *cobra.Command, args []string) {
gotArgs = args
gotVerbose, _ = command.Flags().GetBool("verbose")
},
}
command.Flags().BoolP("verbose", "v", false, "verbose")
configureCommandPassthrough(command)
command.SetArgs([]string{"--verbose", "curl", "--location", "-H", "accept: application/json"})

if err := command.Execute(); err != nil {
t.Fatal(err)
}
if !gotVerbose {
t.Fatal("gg flag before command was not parsed")
}
wantArgs := []string{"curl", "--location", "-H", "accept: application/json"}
if !reflect.DeepEqual(gotArgs, wantArgs) {
t.Fatalf("command args = %#v, want %#v", gotArgs, wantArgs)
}
}
68 changes: 68 additions & 0 deletions cmd/subscription_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cmd

import (
"encoding/base64"
"testing"

"github.com/mzz2017/gg/dialer"
_ "github.com/mzz2017/gg/dialer/anytls"
_ "github.com/mzz2017/gg/dialer/hysteria2"
)

func TestResolveSubscriptionAsClashAnyTLS(t *testing.T) {
config := []byte(`proxies:
- name: clash-anytls
type: anytls
server: 192.0.2.1
port: 443
password: secret
sni: proxy.example
skip-cert-verify: true
udp: false
`)
dialers, err := resolveSubscriptionAsClash(NewLogger(0), &dialer.GlobalOption{}, config)
if err != nil {
t.Fatal(err)
}
if len(dialers) != 1 {
t.Fatalf("dialers = %d, want 1", len(dialers))
}
if dialers[0].Protocol() != "anytls" {
t.Fatalf("protocol = %q, want anytls", dialers[0].Protocol())
}
if dialers[0].SupportUDP() {
t.Fatal("expected UDP support to be disabled")
}
}

func TestResolveSubscriptionAsBase64AnyTLS(t *testing.T) {
link := "anytls://test-auth@192.0.2.1:443?sni=proxy.example&insecure=1#anytls-node"
subscription := []byte(base64.StdEncoding.EncodeToString([]byte(link)))

dialers := resolveSubscriptionAsBase64(NewLogger(0), &dialer.GlobalOption{}, subscription)
if len(dialers) != 1 {
t.Fatalf("dialers = %d, want 1", len(dialers))
}
if got := dialers[0].Protocol(); got != "anytls" {
t.Fatalf("protocol = %q, want anytls", got)
}
if !dialers[0].SupportUDP() {
t.Fatal("expected UDP support")
}
}

func TestResolveSubscriptionAsBase64Hysteria2(t *testing.T) {
link := "hysteria2://secret@192.0.2.1:443?sni=proxy.example&insecure=1#hy2-node"
subscription := []byte(base64.StdEncoding.EncodeToString([]byte(link)))

dialers := resolveSubscriptionAsBase64(NewLogger(0), &dialer.GlobalOption{}, subscription)
if len(dialers) != 1 {
t.Fatalf("dialers = %d, want 1", len(dialers))
}
if got := dialers[0].Protocol(); got != "hysteria2" {
t.Fatalf("protocol = %q, want hysteria2", got)
}
if !dialers[0].SupportUDP() {
t.Fatal("expected UDP support")
}
}
25 changes: 25 additions & 0 deletions dialer/anytls/anytls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Package anytls adapts outbound's AnyTLS share-link dialer to gg.
package anytls

import (
outboundanytls "github.com/daeuniverse/outbound/dialer/anytls"
_ "github.com/daeuniverse/outbound/protocol/anytls"
"github.com/mzz2017/gg/dialer"
"github.com/mzz2017/gg/dialer/outboundlink"
"gopkg.in/yaml.v3"
)

func init() {
dialer.FromLinkRegister("anytls", New)
dialer.FromClashRegister("anytls", NewFromClash)
}

// NewFromClash creates an AnyTLS dialer from a Clash proxy entry.
func NewFromClash(node *yaml.Node, option *dialer.GlobalOption) (*dialer.Dialer, error) {
return outboundlink.NewFromClash(node, option, "anytls", outboundanytls.NewAnytls)
}

// New creates an AnyTLS dialer from a share link.
func New(link string, option *dialer.GlobalOption) (*dialer.Dialer, error) {
return outboundlink.New(link, option, outboundanytls.NewAnytls)
}
63 changes: 63 additions & 0 deletions dialer/anytls/anytls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package anytls

import (
"net/url"
"testing"

"github.com/mzz2017/gg/dialer"
"gopkg.in/yaml.v3"
)

func TestNew(t *testing.T) {
got, err := New(
"anytls://test-auth@192.0.2.1:443?sni=proxy.example&insecure=1#anytls-node",
&dialer.GlobalOption{},
)
if err != nil {
t.Fatal(err)
}
if got.Name() != "anytls-node" {
t.Fatalf("name = %q, want anytls-node", got.Name())
}
if got.Protocol() != "anytls" {
t.Fatalf("protocol = %q, want anytls", got.Protocol())
}
if !got.SupportUDP() {
t.Fatal("expected UDP support")
}
link, err := url.Parse(got.Link())
if err != nil {
t.Fatal(err)
}
if link.Scheme != "anytls" {
t.Fatalf("link scheme = %q, want anytls", link.Scheme)
}
}

func TestNewFromClash(t *testing.T) {
var node yaml.Node
if err := yaml.Unmarshal([]byte("name: clash-anytls\ntype: anytls\nserver: 192.0.2.1\nport: 443\npassword: secret\nsni: proxy.example\nskip-cert-verify: true\n"), &node); err != nil {
t.Fatal(err)
}
got, err := NewFromClash(&node, &dialer.GlobalOption{})
if err != nil {
t.Fatal(err)
}
if got.Name() != "clash-anytls" || got.Protocol() != "anytls" || !got.SupportUDP() {
t.Fatalf("metadata = (%q, %q, udp=%v)", got.Name(), got.Protocol(), got.SupportUDP())
}
}

func TestNewFromClashUDPDisabled(t *testing.T) {
var node yaml.Node
if err := yaml.Unmarshal([]byte("name: clash-anytls\ntype: anytls\nserver: 192.0.2.1\nport: 443\npassword: secret\nudp: false\n"), &node); err != nil {
t.Fatal(err)
}
got, err := NewFromClash(&node, &dialer.GlobalOption{})
if err != nil {
t.Fatal(err)
}
if got.SupportUDP() {
t.Fatal("expected UDP support to be disabled")
}
}
5 changes: 3 additions & 2 deletions dialer/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package http

import (
"fmt"
"github.com/daeuniverse/outbound/protocol/http"
"github.com/mzz2017/gg/common"
"github.com/mzz2017/gg/dialer"
"github.com/mzz2017/softwind/protocol/http"
"gopkg.in/yaml.v3"
"net"
"net/url"
Expand Down Expand Up @@ -114,10 +114,11 @@ func ParseClash(o *yaml.Node) (data *HTTP, err error) {

func (s *HTTP) Dialer() (*dialer.Dialer, error) {
u := s.URL()
d, err := http.NewHTTPProxy(&u, dialer.SymmetricDirect)
outboundDialer, err := http.NewHTTPProxy(&u, dialer.ToNetproxyDialer(dialer.SymmetricDirect))
if err != nil {
return nil, err
}
d := dialer.FromNetproxyDialer(outboundDialer)
return dialer.NewDialer(d, false, s.Name, s.Protocol, u.String()), nil
}

Expand Down
14 changes: 14 additions & 0 deletions dialer/http/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package http

import "testing"

func TestDialerUsesOutbound(t *testing.T) {
h := &HTTP{
Server: "192.0.2.1",
Port: 8080,
Protocol: "http",
}
if _, err := h.Dialer(); err != nil {
t.Fatal(err)
}
}
26 changes: 26 additions & 0 deletions dialer/hysteria2/hysteria2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Package hysteria2 adapts outbound's Hysteria2 dialer to gg.
package hysteria2

import (
outboundhysteria2 "github.com/daeuniverse/outbound/dialer/hysteria2"
_ "github.com/daeuniverse/outbound/protocol/hysteria2"
"github.com/mzz2017/gg/dialer"
"github.com/mzz2017/gg/dialer/outboundlink"
"gopkg.in/yaml.v3"
)

func init() {
dialer.FromLinkRegister("hysteria2", New)
dialer.FromLinkRegister("hy2", New)
dialer.FromClashRegister("hysteria2", NewFromClash)
}

// New creates a Hysteria2 dialer from a share link.
func New(link string, option *dialer.GlobalOption) (*dialer.Dialer, error) {
return outboundlink.New(link, option, outboundhysteria2.NewHysteria2)
}

// NewFromClash creates a Hysteria2 dialer from a Clash proxy entry.
func NewFromClash(node *yaml.Node, option *dialer.GlobalOption) (*dialer.Dialer, error) {
return outboundlink.NewFromClash(node, option, "hysteria2", outboundhysteria2.NewHysteria2)
}
Loading
Loading