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

feat(client): add custom prefix support to the Client library #127

Merged
merged 3 commits into from
Oct 21, 2022
Merged
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
13 changes: 13 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ type Client interface {
// ListenUDP relays UDP packets though a Shadowsocks proxy.
// `laddr` is a local bind address, a local address is automatically chosen if nil.
ListenUDP(laddr *net.UDPAddr) (net.PacketConn, error)

// SetTCPSaltGenerator controls the SaltGenerator used for TCP upstream.
// `salter` may be `nil`.
// This method is not thread-safe.
SetTCPSaltGenerator(ss.SaltGenerator)
}

// NewClient creates a client that routes connections to a Shadowsocks proxy listening at
Expand All @@ -51,6 +56,11 @@ type ssClient struct {
proxyIP net.IP
proxyPort int
cipher *ss.Cipher
salter ss.SaltGenerator
}

func (c *ssClient) SetTCPSaltGenerator(salter ss.SaltGenerator) {
c.salter = salter
}

// This code contains an optimization to send the initial client payload along with
Expand All @@ -76,6 +86,9 @@ func (c *ssClient) DialTCP(laddr *net.TCPAddr, raddr string) (onet.DuplexConn, e
return nil, err
}
ssw := ss.NewShadowsocksWriter(proxyConn, c.cipher)
if c.salter != nil {
ssw.SetSaltGenerator(c.salter)
}
_, err = ssw.LazyWrite(socksTargetAddr)
if err != nil {
proxyConn.Close()
Expand Down
48 changes: 48 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,54 @@ func TestShadowsocksClient_DialTCPFastClose(t *testing.T) {
<-done
}

func TestShadowsocksClient_TCPPrefix(t *testing.T) {
prefix := []byte("test prefix")

listener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
if err != nil {
t.Fatalf("ListenTCP failed: %v", err)
}
var running sync.WaitGroup
running.Add(1)
go func() {
defer running.Done()
defer listener.Close()
clientConn, err := listener.AcceptTCP()
if err != nil {
t.Logf("AcceptTCP failed: %v", err)
return
}
defer clientConn.Close()
prefixReceived := make([]byte, len(prefix))
if _, err := io.ReadFull(clientConn, prefixReceived); err != nil {
t.Error(err)
}
for i := range prefix {
if prefixReceived[i] != prefix[i] {
t.Error("prefix contents mismatch")
}
}
}()

proxyHost, proxyPort, err := splitHostPortNumber(listener.Addr().String())
if err != nil {
t.Fatalf("Failed to parse proxy address: %v", err)
}

d, err := NewClient(proxyHost, proxyPort, testPassword, ss.TestCipher)
if err != nil {
t.Error(err)
}
d.SetTCPSaltGenerator(NewPrefixSaltGenerator(prefix))
conn, err := d.DialTCP(nil, testTargetAddr)
if err != nil {
t.Fatalf("ShadowsocksClient.DialTCP failed: %v", err)
}
conn.Write(nil)
conn.Close()
running.Wait()
}

func TestShadowsocksClient_ListenUDP(t *testing.T) {
proxy, running := startShadowsocksUDPEchoServer(testTargetAddr, t)
proxyHost, proxyPort, err := splitHostPortNumber(proxy.LocalAddr().String())
Expand Down
50 changes: 50 additions & 0 deletions client/salt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2022 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"crypto/rand"
"errors"

ss "github.com/Jigsaw-Code/outline-ss-server/shadowsocks"
)

type prefixSaltGenerator struct {
prefix []byte
}

func (g prefixSaltGenerator) GetSalt(salt []byte) error {
n := copy(salt, g.prefix)
if n != len(g.prefix) {
return errors.New("prefix is too long")
}
_, err := rand.Read(salt[n:])
return err
}

// NewPrefixSaltGenerator returns a SaltGenerator whose output consists of
// the provided prefix, followed by random bytes. This is useful to change
// how shadowsocks traffic is classified by middleboxes.
//
// Note: Prefixes steal entropy from the initialization vector. This weakens
// security by increasing the likelihood that the same IV is used in two
// different connections (which becomes likely once 2^(N/2) connections are
// made, due to the birthday attack). If an IV is reused, the attacker can
// not only decrypt the ciphertext of those two connections; they can also
// easily recover the shadowsocks key and decrypt all other connections to
// this server. Use with care!
func NewPrefixSaltGenerator(prefix []byte) ss.SaltGenerator {
return prefixSaltGenerator{prefix}
}
75 changes: 75 additions & 0 deletions client/salt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2022 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"testing"

ss "github.com/Jigsaw-Code/outline-ss-server/shadowsocks"
)

// setRandomBitsToOne replaces any random bits in the output with 1.
func setRandomBitsToOne(salter ss.SaltGenerator, output []byte) error {
salt := make([]byte, len(output))
// OR together 128 salts. The probability that any random bit is
// 0 for all 128 random salts is 2^-128, which is close enough to zero.
for i := 0; i < 128; i++ {
if err := salter.GetSalt(salt); err != nil {
return err
}
for i := range salt {
output[i] |= salt[i]
}
}
return nil
}

// Test that the prefix bytes are respected, and the remainder are random.
func TestTypicalPrefix(t *testing.T) {
prefix := []byte("twelve bytes")
salter := NewPrefixSaltGenerator(prefix)

output := make([]byte, 32)
if err := setRandomBitsToOne(salter, output); err != nil {
t.Error(err)
}

for i := 0; i < 12; i++ {
if output[i] != prefix[i] {
t.Error("prefix mismatch")
}
}

for _, b := range output[12:] {
if b != 0xFF {
t.Error("unexpected zero bit")
}
}
}

// Test that all bytes are random when the prefix is nil
func TestNilPrefix(t *testing.T) {
salter := NewPrefixSaltGenerator(nil)

output := make([]byte, 64)
if err := setRandomBitsToOne(salter, output); err != nil {
t.Error(err)
}
for _, b := range output {
if b != 0xFF {
t.Error("unexpected zero bit")
}
}
}