Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for various transports, and mechanism to collect connectivity report #137

Closed
wants to merge 52 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
909904e
feature: add report field to the frontend
amircybersec Dec 3, 2023
2d7a657
feat: collect reports in backend +retry/sampling
amircybersec Dec 3, 2023
87da88a
UI: remove prefix, transport optional, add error
amircybersec Dec 6, 2023
40f4ef1
Backend: support for any transport, sanitize ss://
amircybersec Dec 6, 2023
651c520
collect report on on stdout
amircybersec Dec 10, 2023
b4d82ac
define custom popup info element
amircybersec Dec 10, 2023
d608959
reimplment info with popover
amircybersec Dec 12, 2023
5854d2c
add logo element
amircybersec Dec 26, 2023
2feeaea
refactor and improve popover element
amircybersec Dec 26, 2023
87868ea
add logo and popover to the app
amircybersec Dec 26, 2023
79f7fb8
made IsSuccess more readable
amircybersec Dec 26, 2023
bcd51aa
chore: move tls library to transport (#139)
fortuna Dec 8, 2023
3461299
feat(example): config for TLS fragmentation by fixed bytes (#135)
jyyi1 Dec 13, 2023
b288ec5
Fix command
fortuna Dec 21, 2023
a31dc3e
refactor: clean up outline-connectivity (#146)
fortuna Dec 28, 2023
6e73d19
add startTime & Duration to conn operations
amircybersec Jan 1, 2024
3d6e2b5
sanitize config + get hostnames for dns test
amircybersec Jan 3, 2024
b28f3b0
revert timestamp changes
amircybersec Jan 5, 2024
4e22395
generalize config sanitizer remove gethostnames
amircybersec Jan 5, 2024
d76a38f
clean up
amircybersec Jan 5, 2024
4d88238
clean up
amircybersec Jan 5, 2024
6808b92
sanitize logic for each transport + support base64
amircybersec Jan 9, 2024
d2360d3
add test + code cleanup
amircybersec Jan 9, 2024
8ab81c0
check for 64base encoding during url parsing
amircybersec Jan 9, 2024
0913526
add support for both SIP002 and LegacyBase64 URL
amircybersec Jan 10, 2024
28a815e
clean up and extend tests
amircybersec Jan 10, 2024
33e7ac4
clean up sanitizer logic
amircybersec Jan 10, 2024
2ef51e4
parse SIP002 first then fall back to legacy base64
amircybersec Jan 10, 2024
6433f44
sanitize socks5 specifically; removed vless
amircybersec Jan 10, 2024
fcaec8a
cleaned up tests
amircybersec Jan 10, 2024
a2026b9
Update x/config/shadowsocks.go
amircybersec Jan 13, 2024
48300e6
Update x/config/shadowsocks.go
amircybersec Jan 13, 2024
1d05ef1
Update x/config/config.go
amircybersec Jan 13, 2024
5d37408
chore: revamp README (#151)
fortuna Jan 2, 2024
cb14031
feat: introduce Func Endpoints and Dialers (#153)
fortuna Jan 2, 2024
04305b2
chore: test Mobileproxy on CI (#147)
fortuna Jan 2, 2024
011fab6
chore(deps): bump golang.org/x/crypto from 0.7.0 to 0.17.0
dependabot[bot] Dec 19, 2023
abddd05
chore(deps): bump golang.org/x/crypto
dependabot[bot] Dec 18, 2023
84104a8
chore(deps): bump golang.org/x/crypto from 0.14.0 to 0.17.0 in /x
dependabot[bot] Jan 2, 2024
f95745b
feat: introduce address override (#155)
fortuna Jan 9, 2024
b38b491
feat: create DNS library (#141)
fortuna Jan 9, 2024
ca1492f
chore(deps): bump golang.org/x/net from 0.10.0 to 0.17.0
dependabot[bot] Jan 9, 2024
6091a61
chore: update examples to use new dns package (#145)
fortuna Jan 12, 2024
e6695ad
add dynamic config support
amircybersec Jan 16, 2024
4c01a7b
feat: create graphical local proxy app (#164)
fortuna Jan 17, 2024
80a4648
BREAKING: Cleanup core transport library (#165)
fortuna Jan 19, 2024
7d73010
Update iOS WebView interception instructions
daniellacosse Jan 18, 2024
1c7b24e
Revert "add dynamic config support"
amircybersec Jan 21, 2024
ed94c54
rename accessKey to transport
amircybersec Jan 22, 2024
4cc5676
rename accessKey to transport
amircybersec Jan 22, 2024
4e465d7
refactor: new dns pkg & config sanitizer
amircybersec Jan 22, 2024
64aca1d
tidy packages
amircybersec Jan 22, 2024
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
244 changes: 124 additions & 120 deletions x/examples/outline-connectivity-app/shared_backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,22 @@ package shared_backend

import (
"context"
"encoding/base64"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/url"
"runtime"
"strconv"
"strings"
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
"github.com/Jigsaw-Code/outline-sdk/x/config"
"github.com/Jigsaw-Code/outline-sdk/x/connectivity"
"github.com/Jigsaw-Code/outline-sdk/x/report"

_ "golang.org/x/mobile/bind"
)
Expand All @@ -42,16 +43,23 @@ type ConnectivityTestProtocolConfig struct {

type ConnectivityTestResult struct {
// Inputs
Proxy string `json:"proxy"`
Resolver string `json:"resolver"`
Proto string `json:"proto"`
Prefix string `json:"prefix"`
Transport string `json:"transport"`
Resolver string `json:"resolver"`
Proto string `json:"proto"`
// Observations
Time time.Time `json:"time"`
DurationMs int64 `json:"durationMs"`
Error *ConnectivityTestError `json:"error"`
}

func (r ConnectivityTestResult) IsSuccess() bool {
if r.Error == nil {
Copy link
Contributor

@daniellacosse daniellacosse Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just return r.Error == nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fixed

return true
} else {
return false
}
}

type ConnectivityTestError struct {
// TODO: add Shadowsocks/Transport error
Op string `json:"operation"`
Expand All @@ -66,81 +74,103 @@ type ConnectivityTestRequest struct {
Domain string `json:"domain"`
Resolvers []string `json:"resolvers"`
Protocols ConnectivityTestProtocolConfig `json:"protocols"`
ReportTo string `json:"reportTo"`
}

type sessionConfig struct {
Hostname string
Port int
CryptoKey *shadowsocks.EncryptionKey
Prefix Prefix
}
func ConnectivityTest(request ConnectivityTestRequest) ([]ConnectivityTestResult, error) {
var result ConnectivityTestResult
var results []ConnectivityTestResult

type Prefix []byte
transportConfig := replaceSSKeyWithHash(request.AccessKey)

func ConnectivityTest(request ConnectivityTestRequest) ([]ConnectivityTestResult, error) {
accessKeyParameters, err := parseAccessKey(request.AccessKey)
if err != nil {
return nil, err
}
for _, resolverHost := range request.Resolvers {
resolverHost := strings.TrimSpace(resolverHost)
resolverAddress := net.JoinHostPort(resolverHost, "53")
fmt.Printf("ResolverAddress: %v\n", resolverAddress)

proxyIPs, err := net.DefaultResolver.LookupIP(context.Background(), "ip", accessKeyParameters.Hostname)
if err != nil {
return nil, err
}
if request.Protocols.TCP {
testTime := time.Now()
var testErr error
var testDuration time.Duration

// TODO: limit number of IPs. Or force an input IP?
var results []ConnectivityTestResult
for _, hostIP := range proxyIPs {
proxyAddress := net.JoinHostPort(hostIP.String(), fmt.Sprint(accessKeyParameters.Port))

for _, resolverHost := range request.Resolvers {
resolverHost := strings.TrimSpace(resolverHost)
resolverAddress := net.JoinHostPort(resolverHost, "53")

if request.Protocols.TCP {
testTime := time.Now()
var testErr error
var testDuration time.Duration

streamDialer, err := config.NewStreamDialer("")
if err != nil {
log.Fatalf("Failed to create StreamDialer: %v", err)
}
streamDialer, err := config.NewStreamDialer(request.AccessKey)
if err != nil {
//log.Fatalf("Failed to create StreamDialer: %v", err)
testDuration = time.Duration(0)
testErr = err
} else {
resolver := &transport.StreamDialerEndpoint{Dialer: streamDialer, Address: resolverAddress}
testDuration, testErr = connectivity.TestResolverStreamConnectivity(context.Background(), resolver, resolverAddress)

results = append(results, ConnectivityTestResult{
Proxy: proxyAddress,
Resolver: resolverAddress,
Proto: "tcp",
Prefix: accessKeyParameters.Prefix.String(),
Time: testTime.UTC().Truncate(time.Second),
DurationMs: testDuration.Milliseconds(),
Error: makeErrorRecord(testErr),
})
fmt.Printf("TestDuration: %v\n", testDuration)
fmt.Printf("TestError: %v\n", testErr)
}
result = ConnectivityTestResult{
Transport: transportConfig,
Resolver: resolverAddress,
Proto: "tcp",
Time: testTime.UTC().Truncate(time.Second),
DurationMs: testDuration.Milliseconds(),
Error: makeErrorRecord(testErr),
}
results = append(results, result)
}

if request.Protocols.UDP {
testTime := time.Now()
var testErr error
var testDuration time.Duration

packetDialer, err := config.NewPacketDialer("")
if err != nil {
log.Fatalf("Failed to create PacketDialer: %v", err)
}
if request.Protocols.UDP {
testTime := time.Now()
var testErr error
var testDuration time.Duration

packetDialer, err := config.NewPacketDialer(request.AccessKey)
if err != nil {
//log.Fatalf("Failed to create PacketDialer: %v", err)
testDuration = time.Duration(0)
testErr = err
} else {
resolver := &transport.PacketDialerEndpoint{Dialer: packetDialer, Address: resolverAddress}
testDuration, testErr = connectivity.TestResolverPacketConnectivity(context.Background(), resolver, resolverAddress)
fmt.Printf("TestDuration: %v\n", testDuration)
fmt.Printf("TestError: %v\n", testErr)
}

results = append(results, ConnectivityTestResult{
Proxy: proxyAddress,
Resolver: resolverAddress,
Proto: "udp",
Prefix: accessKeyParameters.Prefix.String(),
Time: testTime.UTC().Truncate(time.Second),
DurationMs: testDuration.Milliseconds(),
Error: makeErrorRecord(testErr),
})
result = ConnectivityTestResult{
Transport: transportConfig,
Resolver: resolverAddress,
Proto: "udp",
Time: testTime.UTC().Truncate(time.Second),
DurationMs: testDuration.Milliseconds(),
Error: makeErrorRecord(testErr),
}
results = append(results, result)
}
}
for _, result := range results {
fmt.Printf("Result: %v\n", result)
var r report.Report = result
u, err := url.Parse(request.ReportTo)
if err != nil {
log.Printf("Expected no error, but got: %v", err)
//return results, errors.New("failed to parse collector URL")
}
fmt.Println("Parsed URL: ", u.String())
if u.String() != "" {
remoteCollector := &report.RemoteCollector{
CollectorURL: u,
HttpClient: &http.Client{Timeout: 10 * time.Second},
}
retryCollector := &report.RetryCollector{
Collector: remoteCollector,
MaxRetry: 3,
InitialDelay: 1 * time.Second,
}
c := report.SamplingCollector{
Collector: retryCollector,
SuccessFraction: 0.1,
FailureFraction: 1.0,
}
err = c.Collect(context.Background(), r)
if err != nil {
log.Printf("Failed to collect report: %v\n", err)
//return results, errors.New("failed to collect report")
}
}
}
Expand Down Expand Up @@ -182,60 +212,34 @@ func unwrapAll(err error) error {
}
}

func (p Prefix) String() string {
runes := make([]rune, len(p))
for i, b := range p {
runes[i] = rune(b)
}
return string(runes)
}
// hashKey hashes the given key and returns the hexadecimal representation
func hashKey(key string) string {
hasher := sha256.New()
hasher.Write([]byte(key))
fullHash := hex.EncodeToString(hasher.Sum(nil))
return fullHash[:10] // Truncate the hash to 10 characters

func parseAccessKey(accessKey string) (*sessionConfig, error) {
var config sessionConfig
accessKeyURL, err := url.Parse(accessKey)
if err != nil {
return nil, fmt.Errorf("failed to parse access key: %w", err)
}
var portString string
// Host is a <host>:<port> string
config.Hostname, portString, err = net.SplitHostPort(accessKeyURL.Host)
if err != nil {
return nil, fmt.Errorf("failed to parse endpoint address: %w", err)
}
config.Port, err = strconv.Atoi(portString)
if err != nil {
return nil, fmt.Errorf("failed to parse port number: %w", err)
}
cipherInfoBytes, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(accessKeyURL.User.String())
if err != nil {
return nil, fmt.Errorf("failed to decode cipher info [%v]: %v", accessKeyURL.User.String(), err)
}
cipherName, secret, found := strings.Cut(string(cipherInfoBytes), ":")
if !found {
return nil, fmt.Errorf("invalid cipher info: no ':' separator")
}
config.CryptoKey, err = shadowsocks.NewEncryptionKey(cipherName, secret)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
prefixStr := accessKeyURL.Query().Get("prefix")
if len(prefixStr) > 0 {
config.Prefix, err = ParseStringPrefix(prefixStr)
if err != nil {
return nil, fmt.Errorf("failed to parse prefix: %w", err)
}
}
return &config, nil
}

func ParseStringPrefix(utf8Str string) (Prefix, error) {
runes := []rune(utf8Str)
rawBytes := make([]byte, len(runes))
for i, r := range runes {
if (r & 0xFF) != r {
return nil, fmt.Errorf("character out of range: %d", r)
// replaceSSKeyWithHash function replaces the key value with its hash in parts that start with "ss://"
func replaceSSKeyWithHash(input string) string {
// Split the string into parts
parts := strings.Split(input, "|")

// Iterate through each part
for i, part := range parts {
if strings.HasPrefix(part, "ss://") {
// Find the key part and replace it with its hash
keyStart := strings.Index(part, "//") + 2
keyEnd := strings.Index(part, "@")
if keyStart != -1 && keyEnd != -1 && keyEnd > keyStart {
key := part[keyStart:keyEnd]
hashedKey := hashKey(key)
parts[i] = part[:keyStart] + hashedKey + part[keyEnd:]
}
}
rawBytes[i] = byte(r)
}
return rawBytes, nil

// Join the parts back into a string
return strings.Join(parts, "|")
}
Loading