-
Notifications
You must be signed in to change notification settings - Fork 189
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 websocket tunneling support #131
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package client | ||
|
||
import ( | ||
"crypto/tls" | ||
"errors" | ||
"fmt" | ||
"net" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
onet "github.com/Jigsaw-Code/outline-ss-server/net" | ||
ss "github.com/Jigsaw-Code/outline-ss-server/shadowsocks" | ||
"github.com/Jigsaw-Code/outline-ss-server/websocket" | ||
"github.com/shadowsocks/go-shadowsocks2/socks" | ||
) | ||
|
||
type WebsocketOptions struct { | ||
// Addr is the address of the websocket server. It can either an IP address or a domain name. | ||
Addr string | ||
// Port is the destination port of the websocket connection. | ||
Port int | ||
// Host is the hostname to use in the Host header of HTTP request made to the websocket server. | ||
// If empty, the header will be set to `Addr` if it is a domain name. | ||
Host string | ||
// SNI is the hostname to use in the server name extension of the TLS handshake. If empty, it will be set to `Host`. | ||
SNI string | ||
// Path is the HTTP path to use when connecting to the websocket server. | ||
Path string | ||
// Password is the password to use for the shadowsocks connection tunnelled inside the websocket connection. | ||
Password string | ||
// Ciphter is the cipher to use for the shadowsocks connection tunnelled inside the websocket connection. | ||
Cipher string | ||
} | ||
|
||
// NewWebsocketClient creates a client that routes connections to a Shadowsocks proxy | ||
// tunneled inside a websocket connection. | ||
func NewWebsocketClient(opts WebsocketOptions) (Client, error) { | ||
proxy := opts.Addr | ||
if proxy == "" { | ||
proxy = opts.Host | ||
} | ||
if proxy == "" { | ||
return nil, fmt.Errorf("neither Addr or Host are defined") | ||
} | ||
|
||
ss, err := NewClient(proxy, opts.Port, opts.Password, opts.Cipher) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if strings.HasPrefix(opts.Path, "/") { | ||
opts.Path = opts.Path[1:] | ||
} | ||
|
||
addrIP := net.ParseIP(opts.Addr) | ||
if opts.Host == "" && addrIP == nil { | ||
opts.Host = opts.Addr | ||
} | ||
|
||
if opts.SNI == "" { | ||
opts.SNI = opts.Host | ||
} | ||
|
||
return &wsClient{ | ||
ssClient: ss.(*ssClient), | ||
opts: opts, | ||
}, nil | ||
} | ||
|
||
type wsClient struct { | ||
*ssClient | ||
opts WebsocketOptions | ||
} | ||
|
||
func (c *wsClient) DialTCP(laddr *net.TCPAddr, raddr string) (onet.DuplexConn, error) { | ||
socksTargetAddr := socks.ParseAddr(raddr) | ||
if socksTargetAddr == nil { | ||
return nil, errors.New("Failed to parse target address") | ||
} | ||
|
||
h := make(http.Header) | ||
if c.opts.Host != "" { | ||
h.Set("Host", c.opts.Host) | ||
} | ||
d := websocket.Dialer{ | ||
TLSClientConfig: &tls.Config{ | ||
InsecureSkipVerify: true, | ||
ServerName: c.opts.SNI, | ||
}, | ||
HandshakeTimeout: websocket.DefaultHandshakeTimeout, | ||
} | ||
proxyConn, err := d.Dial(fmt.Sprintf("wss://%s:%d/%s", c.proxyIP, c.opts.Port, c.opts.Path), h) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
ssw := ss.NewShadowsocksWriter(proxyConn, c.cipher) | ||
_, err = ssw.LazyWrite(socksTargetAddr) | ||
if err != nil { | ||
proxyConn.Close() | ||
return nil, errors.New("Failed to write target address") | ||
} | ||
time.AfterFunc(helloWait, func() { | ||
ssw.Flush() | ||
}) | ||
ssr := ss.NewShadowsocksReader(proxyConn, c.cipher) | ||
return onet.WrapConn(proxyConn, ssr, ssw), nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package client | ||
|
||
import ( | ||
"crypto/tls" | ||
"net" | ||
"net/http" | ||
"testing" | ||
"time" | ||
|
||
onet "github.com/Jigsaw-Code/outline-ss-server/net" | ||
ss "github.com/Jigsaw-Code/outline-ss-server/shadowsocks" | ||
"github.com/Jigsaw-Code/outline-ss-server/websocket" | ||
) | ||
|
||
const ( | ||
testWSPath = "/test" | ||
) | ||
|
||
func TestWebsocketClient(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
opts WebsocketOptions | ||
wantHost string | ||
wantSNI string | ||
}{ | ||
{ | ||
name: "with_ip_host", | ||
opts: WebsocketOptions{Addr: "127.0.0.1", Host: "example.com"}, | ||
wantHost: "example.com", | ||
wantSNI: "example.com", | ||
}, | ||
{ | ||
name: "with_ip_host_sni", | ||
opts: WebsocketOptions{Addr: "127.0.0.1", Host: "example.com", SNI: "sni.com"}, | ||
wantHost: "example.com", | ||
wantSNI: "sni.com", | ||
}, | ||
{ | ||
name: "with_domain", | ||
opts: WebsocketOptions{Addr: "localhost"}, | ||
wantHost: "localhost", | ||
wantSNI: "localhost", | ||
}, | ||
{ | ||
name: "with_domain_host", | ||
opts: WebsocketOptions{Addr: "localhost", Host: "example.com"}, | ||
wantHost: "example.com", | ||
wantSNI: "example.com", | ||
}, | ||
{ | ||
name: "with_domain_host_sni", | ||
opts: WebsocketOptions{Addr: "localhost", Host: "example.com", SNI: "sni.com"}, | ||
wantHost: "example.com", | ||
wantSNI: "sni.com", | ||
}, | ||
} | ||
|
||
proxy, hostCh, sniCh := startWebsocketShadowsocksEchoProxy(t) | ||
defer close(hostCh) | ||
defer close(sniCh) | ||
defer proxy.Close() | ||
_, proxyPort, err := splitHostPortNumber(proxy.Addr().String()) | ||
if err != nil { | ||
t.Fatalf("Failed to parse proxy address: %v", err) | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
tc.opts.Password = testPassword | ||
tc.opts.Cipher = ss.TestCipher | ||
tc.opts.Port = proxyPort | ||
tc.opts.Path = testWSPath | ||
|
||
d, err := NewWebsocketClient(tc.opts) | ||
if err != nil { | ||
t.Fatalf("Failed to create WebsocketClient: %v", err) | ||
} | ||
conn, err := d.DialTCP(nil, testTargetAddr) | ||
if err != nil { | ||
t.Fatalf("WebsocketClient.DialTCP failed: %v", err) | ||
} | ||
|
||
select { | ||
case sni := <-sniCh: | ||
if sni != tc.wantSNI { | ||
t.Fatalf("Wrong server name in TLS handshake server. got='%v' want='%v'", sni, tc.wantSNI) | ||
} | ||
case <-time.After(50 * time.Millisecond): | ||
t.Fatal("TLS connection state not recevied") | ||
} | ||
select { | ||
case host := <-hostCh: | ||
if host != tc.wantHost { | ||
t.Fatalf("Wrong host header. got='%v' want='%v'", host, tc.wantHost) | ||
} | ||
case <-time.After(50 * time.Millisecond): | ||
t.Fatal("HTTP request not recevied") | ||
} | ||
|
||
conn.SetReadDeadline(time.Now().Add(time.Second * 5)) | ||
expectEchoPayload(conn, ss.MakeTestPayload(1024), make([]byte, 1024), t) | ||
conn.Close() | ||
}) | ||
} | ||
} | ||
|
||
func startWebsocketShadowsocksEchoProxy(t *testing.T) (net.Listener, chan string, chan string) { | ||
proxy, _ := startShadowsocksTCPEchoProxy(testTargetAddr, t) | ||
|
||
hostCh := make(chan string, 1) | ||
sniCh := make(chan string, 1) | ||
|
||
handler := func(w http.ResponseWriter, r *http.Request) { | ||
u := websocket.Upgrader{HandshakeTimeout: 50 * time.Millisecond} | ||
c, err := u.Upgrade(w, r, nil) | ||
defer c.Close() | ||
|
||
hostCh <- r.Host | ||
|
||
if r.URL.Path != testWSPath { | ||
t.Logf("Wrong Path received on request. got='%v' want='%v'", testWSPath, r.URL.Path) | ||
return | ||
} | ||
|
||
targetC, err := net.Dial("tcp", proxy.Addr().String()) | ||
if err != nil { | ||
t.Logf("Failed to connect to TCP echo server: %v", err) | ||
return | ||
} | ||
|
||
onet.Relay(c, targetC.(*net.TCPConn)) | ||
} | ||
|
||
l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) | ||
if err != nil { | ||
t.Fatalf("Starting websocket listener failed: %v", err) | ||
} | ||
|
||
go func() { | ||
srv := &http.Server{Handler: http.HandlerFunc(handler)} | ||
srv.TLSConfig = &tls.Config{ | ||
VerifyConnection: func(cs tls.ConnectionState) error { | ||
sniCh <- cs.ServerName | ||
return nil | ||
}, | ||
} | ||
srv.ServeTLS(l, websocket.TestCert, websocket.TestKey) | ||
}() | ||
|
||
return l, hostCh, sniCh | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -212,19 +212,25 @@ func readConfig(filename string) (*Config, error) { | |
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert the service. |
||
func main() { | ||
var flags struct { | ||
ConfigFile string | ||
MetricsAddr string | ||
IPCountryDB string | ||
natTimeout time.Duration | ||
replayHistory int | ||
Verbose bool | ||
Version bool | ||
ConfigFile string | ||
MetricsAddr string | ||
IPCountryDB string | ||
natTimeout time.Duration | ||
replayHistory int | ||
Verbose bool | ||
Version bool | ||
WebsocketServer bool | ||
TLSCert string | ||
TLSKey string | ||
} | ||
flag.StringVar(&flags.ConfigFile, "config", "", "Configuration filename") | ||
flag.StringVar(&flags.MetricsAddr, "metrics", "", "Address for the Prometheus metrics") | ||
flag.StringVar(&flags.IPCountryDB, "ip_country_db", "", "Path to the ip-to-country mmdb file") | ||
flag.DurationVar(&flags.natTimeout, "udptimeout", defaultNatTimeout, "UDP tunnel timeout") | ||
flag.IntVar(&flags.replayHistory, "replay_history", 0, "Replay buffer size (# of handshakes)") | ||
flag.BoolVar(&flags.WebsocketServer, "websocket", false, "Enables the websocket serve") | ||
flag.StringVar(&flags.TLSCert, "tls-cert", "ssl.crt", "Path to tls certificate to use for the websocket server") | ||
flag.StringVar(&flags.TLSKey, "tls-key", "ssl.key", "Path to tls key to use for the websocket server") | ||
flag.BoolVar(&flags.Verbose, "verbose", false, "Enables verbose logging output") | ||
flag.BoolVar(&flags.Version, "version", false, "The version of the server") | ||
|
||
|
@@ -266,11 +272,19 @@ func main() { | |
} | ||
m := metrics.NewPrometheusShadowsocksMetrics(ipCountryDB, prometheus.DefaultRegisterer) | ||
m.SetBuildInfo(version) | ||
_, err = RunSSServer(flags.ConfigFile, flags.natTimeout, m, flags.replayHistory) | ||
ssServer, err := RunSSServer(flags.ConfigFile, flags.natTimeout, m, flags.replayHistory) | ||
if err != nil { | ||
logger.Fatal(err) | ||
} | ||
|
||
if flags.WebsocketServer { | ||
if flags.TLSCert == "" || flags.TLSKey == "" { | ||
log.Fatalln("TLS cert and key not specified") | ||
flag.Usage() | ||
} | ||
RunWebsocketServer(ssServer, 443, flags.TLSCert, flags.TLSKey) | ||
} | ||
|
||
sigCh := make(chan os.Signal, 1) | ||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) | ||
<-sigCh | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -143,6 +143,8 @@ type TCPService interface { | |
Stop() error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Revert |
||
// GracefulStop calls Stop(), and then blocks until all resources have been cleaned up. | ||
GracefulStop() error | ||
// HandleConnection takes a shadowsocks client connection and starts a relay to the destination address. | ||
HandleConnection(listenerPort int, clientTCPConn onet.DuplexConn) | ||
} | ||
|
||
func (s *tcpService) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) { | ||
|
@@ -207,12 +209,12 @@ func (s *tcpService) Serve(listener *net.TCPListener) error { | |
logger.Errorf("Panic in TCP handler: %v", r) | ||
} | ||
}() | ||
s.handleConnection(listener.Addr().(*net.TCPAddr).Port, clientTCPConn) | ||
s.HandleConnection(listener.Addr().(*net.TCPAddr).Port, clientTCPConn) | ||
}() | ||
} | ||
} | ||
|
||
func (s *tcpService) handleConnection(listenerPort int, clientTCPConn *net.TCPConn) { | ||
func (s *tcpService) HandleConnection(listenerPort int, clientTCPConn onet.DuplexConn) { | ||
clientLocation, err := s.m.GetLocation(clientTCPConn.RemoteAddr()) | ||
if err != nil { | ||
logger.Warningf("Failed location lookup: %v", err) | ||
|
@@ -221,7 +223,9 @@ func (s *tcpService) handleConnection(listenerPort int, clientTCPConn *net.TCPCo | |
s.m.AddOpenTCPConnection(clientLocation) | ||
|
||
connStart := time.Now() | ||
clientTCPConn.SetKeepAlive(true) | ||
if tcp, ok := clientTCPConn.(*net.TCPConn); ok { | ||
tcp.SetKeepAlive(true) | ||
} | ||
// Set a deadline to receive the address to the target. | ||
clientTCPConn.SetReadDeadline(connStart.Add(s.readTimeout)) | ||
var proxyMetrics metrics.ProxyMetrics | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
-----BEGIN CERTIFICATE----- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this file? Just a test example? If so, give a clearer name. Also, move it to the websocket folder. |
||
MIIElDCCAnwCCQDlCwAerg6zUzANBgkqhkiG9w0BAQsFADAMMQowCAYDVQQDDAEq | ||
MB4XDTIyMTAzMTAyMDAxMloXDTI1MDcyNzAyMDAxMlowDDEKMAgGA1UEAwwBKjCC | ||
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOV0rN2wcoWuwHtvYyjKO3NL | ||
WQSCB1u/bDrJYXEFCjCaQFuEVWjW44vHwBnTanWPt2TcEN8ZZ7Nd3fku8ZN7I7i9 | ||
3CPRUDatBD/IE6s90JY0dR8i4ZavOorTD8D8Xg5ioh9KdyEnEmgZpDjzENBEJQZa | ||
jUMytEwM5BJYO1QY6TInRbC0XCigouPBdhHR88kc5e/+tAqY7UC5x48CkLIcDAxQ | ||
IzPBdGWheSPjLYLn6XM0QkatZK6/7q40f03BMt29AVvwN3XgU9xBwr4Iij5nC3AV | ||
RfwBbySx+d+5rIxfsm6L5xOP0Zm5IueWNyCa8JDYrxdOf35bcm9VdPvay0IDp4Dr | ||
gTqWRvLWzBCmsXKdOIDo3O0W3UcSfzjyiue+VKNJFOFEpS02RfqaQZ4k+YpS1QIz | ||
3CyzDb7GVq4gNTO7P4hcMXvWxJPqoA6QYAeboHMQr695ucdjF4hebbVMNajXndhf | ||
BPmvAGKZivUn/PZND1vTj331mqWUMQTJOyvUVv+ZpEobhr3GQvScbFw5zLOaR5ud | ||
LAxgmtcyspCC4MQhu7bNPtnP5jhBXmdiNf7bbUQFgnaGDrNKELMIUeY3/TLAs6cv | ||
U+ZbrbofL3eGMZtCeH/7izZxX2cS1OxcHNesqOU7+ZLznbu7AXGrDo6lH/4VbC44 | ||
YaKbn4oxfplUpQcmWWvJAgMBAAEwDQYJKoZIhvcNAQELBQADggIBABdLOenVrN4/ | ||
D2NZJiD027LVcxnWTEjCCMgoaZ1eeQdaHlpQueL1hpZDTnFCZEsbnp77GFqXhNt4 | ||
lnUgF4n8JmFoqR39MCu76k7VlndLTG2aMPOrc6zfe2JUeaC4Q6/BwothMu1xuz6P | ||
kbWResrXVIbdVH+NPqN3SFzye8MQzBkNcgiNRY9syzaPXUD3OfOYT6xnUU6orqsX | ||
LWnCuakRK9YlaK9X6BdPen12wyAbKg+M4eUMTnC/VTRFjHz8H8CnYjk/bzoxUQZG | ||
QV9XK+2dw0A0MLNXiWtoUmemS/ty+tSMnvEMdfXyskJcP3qVbywp4G4E1cUiHw9z | ||
e+0xYZQnaX0I6ztZWliK8ELHEucS+M20VODVUEryKl/zpqq7Rpx16T4VRrnqtYj2 | ||
MqGLWhjUvDRFsuDaVCTbP8oZS3CrR6p0pfHqatYXcRY8E1YeMaGBMz5DKpgEhbjo | ||
2x6md9E7LWVRa4FQu0N/V5xlTUSGUzzgLAXIXhLiKT0B+FutG914zN8z6AOQ7DMY | ||
IhhosVG535/mL8AnPAoDorOSz5Vk1OxPWYCLFiUZK8rqcP3+z0xe4S/bsoGaz7Oc | ||
J/LzGxaanEKi53lS3zg/N4QYWmWe++l59fZVPa0HiAPsMCoHnOGPuhnULzGeGuPJ | ||
+bLSo9WYo8HwVFk6RHeKkJcDoYRQIyO+ | ||
-----END CERTIFICATE----- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Websocket code shouldn't know about Shadowsocks.
Instead, make the Shadowsocks client code take a server Dialer.
Then the server dialer could be a direct connection, or a Websocket connection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With that approach, we don't need to worry about the wiring here. outline-go-tun2socks will have the code that takes a config and translates that to object wiring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need to check how much bigger outline-go-tun2socks will be with this change, and how it will affect memory on iOS.