diff --git a/x/config/config.go b/x/config/config.go index a2dca887..a93b8517 100644 --- a/x/config/config.go +++ b/x/config/config.go @@ -119,3 +119,22 @@ func newPacketDialerFromPart(innerDialer transport.PacketDialer, oneDialerConfig return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme) } } + +// NewShadowsocksPacketListenerFromPart creates a new [transport.PacketListener] according to the given config, +// the config must contain only one "ss://" segment. +func NewShadowsocksPacketListenerFromPart(ssConfig string) (transport.PacketListener, error) { + ssConfig = strings.TrimSpace(ssConfig) + if ssConfig == "" { + return nil, errors.New("empty config part") + } + + url, err := url.Parse(ssConfig) + if err != nil { + return nil, fmt.Errorf("failed to parse config part: %w", err) + } + + if url.Scheme != "ss" { + return nil, errors.New("config scheme must be 'ss' for a PacketListener") + } + return newShadowsocksPacketListenerFromURL(url) +} diff --git a/x/config/shadowsocks.go b/x/config/shadowsocks.go index 5d56cd68..c1c74818 100644 --- a/x/config/shadowsocks.go +++ b/x/config/shadowsocks.go @@ -55,6 +55,15 @@ func newShadowsocksPacketDialerFromURL(innerDialer transport.PacketDialer, confi return dialer, nil } +func newShadowsocksPacketListenerFromURL(configURL *url.URL) (transport.PacketListener, error) { + config, err := parseShadowsocksURL(configURL) + if err != nil { + return nil, err + } + ep := &transport.UDPEndpoint{Address: config.serverAddress} + return shadowsocks.NewPacketListener(ep, config.cryptoKey) +} + type shadowsocksConfig struct { serverAddress string cryptoKey *shadowsocks.EncryptionKey diff --git a/x/examples/outline-cli/main.go b/x/examples/outline-cli/main.go index 0d83b546..cfded5f8 100644 --- a/x/examples/outline-cli/main.go +++ b/x/examples/outline-cli/main.go @@ -17,11 +17,11 @@ package main import ( + "flag" "fmt" "net" "os" "os/signal" - "strconv" "sync" "github.com/vishvananda/netlink" @@ -31,32 +31,17 @@ import ( const OUTLINE_TUN_NAME = "outline233" const OUTLINE_TUN_IP = "10.233.233.1" const OUTLINE_TUN_MTU = 1500 // todo: we can read this from netlink -// const OUTLINE_TUN_SUBNET = "10.233.233.1/32" const OUTLINE_GW_SUBNET = "10.233.233.2/32" const OUTLINE_GW_IP = "10.233.233.2" const OUTLINE_ROUTING_PRIORITY = 23333 const OUTLINE_ROUTING_TABLE = 233 -// ./app -// -// : the outline server IP (e.g. 111.111.111.111) -// : the outline server port (e.g. 21532) -// : the outline server password +// ./app -transport "ss://..." func main() { - fmt.Println("OutlineVPN CLI (experimental-08031526)") + fmt.Println("OutlineVPN CLI (experimental-10161603)") - svrIp := os.Args[1] - svrIpCidr := svrIp + "/32" - svrPass := os.Args[3] - svrPort, err := strconv.Atoi(os.Args[2]) - if err != nil { - fmt.Printf("fatal error: %v\n", err) - return - } - if svrPort < 1000 || svrPort > 65535 { - fmt.Printf("fatal error: server port out of range\n") - return - } + transportFlag := flag.String("transport", "", "Transport config") + flag.Parse() bgWait := &sync.WaitGroup{} defer bgWait.Wait() @@ -68,12 +53,7 @@ func main() { } defer tun.Close() - ss, err := NewOutlineDevice(&OutlineConfig{ - Hostname: svrIp, - Port: uint16(svrPort), - Password: svrPass, - Cipher: "chacha20-ietf-poly1305", - }) + ss, err := NewOutlineDevice(*transportFlag) if err != nil { fmt.Printf("fatal error: %v", err) return @@ -96,40 +76,19 @@ func main() { } defer cleanUpRouting() - if err := showRouting(); err != nil { - return - } - + svrIpCidr := ss.GetServerIP().String() + "/32" r, err := setupIpRule(svrIpCidr) if err != nil { return } defer cleanUpRule(r) - if err := showAllRules(); err != nil { - return - } - sigc := make(chan os.Signal, 1) signal.Notify(sigc, os.Interrupt, unix.SIGTERM, unix.SIGHUP) s := <-sigc fmt.Printf("\nReceived %v, cleaning up resources...\n", s) } -func showRouting() error { - filter := netlink.Route{Table: OUTLINE_ROUTING_TABLE} - routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, &filter, netlink.RT_FILTER_TABLE) - if err != nil { - fmt.Printf("fatal error: %v\n", err) - return err - } - fmt.Printf("\tRoutes (@%v): %v\n", OUTLINE_ROUTING_TABLE, len(routes)) - for _, route := range routes { - fmt.Printf("\t\t%v\n", route) - } - return nil -} - func setupRouting() error { fmt.Println("configuring outline routing table...") tun, err := netlink.LinkByName(OUTLINE_TUN_NAME) @@ -194,18 +153,6 @@ func cleanUpRouting() error { return lastErr } -func showAllRules() error { - rules, err := netlink.RuleList(netlink.FAMILY_ALL) - if err != nil { - fmt.Printf("fatal error: %v\n", err) - return err - } - for _, r := range rules { - fmt.Printf("\t%v\n", r) - } - return nil -} - func setupIpRule(svrIp string) (*netlink.Rule, error) { fmt.Println("adding ip rule for outline routing table...") dst, err := netlink.ParseIPNet(svrIp) diff --git a/x/examples/outline-cli/outline_device.go b/x/examples/outline-cli/outline_device.go index 454b0282..1d338a72 100644 --- a/x/examples/outline-cli/outline_device.go +++ b/x/examples/outline-cli/outline_device.go @@ -15,20 +15,18 @@ package main import ( - "context" "errors" "fmt" "io" "net" - "strconv" + "net/url" + "strings" "sync" - "github.com/Jigsaw-Code/outline-internal-sdk/network" - "github.com/Jigsaw-Code/outline-internal-sdk/network/dnstruncate" - "github.com/Jigsaw-Code/outline-internal-sdk/network/lwip2transport" - "github.com/Jigsaw-Code/outline-internal-sdk/transport" - "github.com/Jigsaw-Code/outline-internal-sdk/transport/shadowsocks" - "github.com/Jigsaw-Code/outline-internal-sdk/x/connectivity" + "github.com/Jigsaw-Code/outline-sdk/network" + "github.com/Jigsaw-Code/outline-sdk/network/lwip2transport" + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/x/config" ) const ( @@ -36,64 +34,29 @@ const ( connectivityTestResolver = "1.1.1.1:53" ) -type OutlineConfig struct { - Hostname string - Port uint16 - Password string - Cipher string -} - type OutlineDevice struct { - t2s network.IPDevice - pktProxy network.DelegatePacketProxy - fallbackPktProxy network.PacketProxy - ssStreamDialer transport.StreamDialer - ssPktListener transport.PacketListener - ssPktProxy network.PacketProxy + t2s network.IPDevice + sd transport.StreamDialer + pp *outlinePacketProxy + svrIP net.IP } -func NewOutlineDevice(config *OutlineConfig) (od *OutlineDevice, err error) { - od = &OutlineDevice{} - - cipher, err := shadowsocks.NewEncryptionKey(config.Cipher, config.Password) - if err != nil { - return nil, fmt.Errorf("failed to create cipher `%v`: %w", config.Cipher, err) - } - - ssAddress := net.JoinHostPort(config.Hostname, strconv.Itoa(int(config.Port))) - - // Create Shadowsocks TCP StreamDialer - od.ssStreamDialer, err = shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: ssAddress}, cipher) +func NewOutlineDevice(transportConfig string) (od *OutlineDevice, err error) { + ip, err := resolveShadowsocksServerIPFromConfig(transportConfig) if err != nil { - return nil, fmt.Errorf("failed to create TCP dialer: %w", err) + return nil, err } - - // Create DNS Truncated PacketProxy - od.fallbackPktProxy, err = dnstruncate.NewPacketProxy() - if err != nil { - return nil, fmt.Errorf("failed to create DNS truncate proxy: %w", err) + od = &OutlineDevice{ + svrIP: ip, } - // Create Shadowsocks UDP PacketProxy - od.ssPktListener, err = shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: ssAddress}, cipher) - if err != nil { - return nil, fmt.Errorf("failed to create UDP listener: %w", err) - } - - od.ssPktProxy, err = network.NewPacketProxyFromPacketListener(od.ssPktListener) - if err != nil { - return nil, fmt.Errorf("failed to create UDP proxy: %w", err) + if od.sd, err = config.NewStreamDialer(transportConfig); err != nil { + return nil, fmt.Errorf("failed to create TCP dialer: %w", err) } - - // Create DelegatePacketProxy - od.pktProxy, err = network.NewDelegatePacketProxy(od.fallbackPktProxy) - if err != nil { + if od.pp, err = newOutlinePacketProxy(transportConfig); err != nil { return nil, fmt.Errorf("failed to create delegate UDP proxy: %w", err) } - - // Configure lwIP Device - od.t2s, err = lwip2transport.ConfigureDevice(od.ssStreamDialer, od.pktProxy) - if err != nil { + if od.t2s, err = lwip2transport.ConfigureDevice(od.sd, od.pp); err != nil { return nil, fmt.Errorf("failed to configure lwIP: %w", err) } @@ -105,26 +68,11 @@ func (d *OutlineDevice) Close() error { } func (d *OutlineDevice) Refresh() error { - fmt.Println("debug: testing TCP connectivity...") - streamResolver := &transport.StreamDialerEndpoint{Dialer: d.ssStreamDialer, Address: connectivityTestResolver} - _, err := connectivity.TestResolverStreamConnectivity(context.Background(), streamResolver, connectivityTestDomain) - if err != nil { - return fmt.Errorf("failed to connect to the remote Shadowsocks server: %w", err) - } - - fmt.Println("debug: testing UDP connectivity...") - dialer := transport.PacketListenerDialer{Listener: d.ssPktListener} - packetResolver := &transport.PacketDialerEndpoint{Dialer: dialer, Address: connectivityTestResolver} - _, err = connectivity.TestResolverPacketConnectivity(context.Background(), packetResolver, connectivityTestDomain) - fmt.Printf("debug: UDP connectivity test result: %v\n", err) + return d.pp.testConnectivityAndRefresh(connectivityTestResolver, connectivityTestDomain) +} - if err != nil { - fmt.Println("info: remote Shadowsocks server doesn't support UDP, switching to local DNS truncation handler") - return d.pktProxy.SetProxy(d.fallbackPktProxy) - } else { - fmt.Println("info: remote Shadowsocks server supports UDP traffic") - return d.pktProxy.SetProxy(d.ssPktProxy) - } +func (d *OutlineDevice) GetServerIP() net.IP { + return d.svrIP } func (d *OutlineDevice) RelayTraffic(netDev io.ReadWriter) error { @@ -155,3 +103,31 @@ func (d *OutlineDevice) RelayTraffic(netDev io.ReadWriter) error { return errors.Join(err1, err2) } + +func resolveShadowsocksServerIPFromConfig(transportConfig string) (net.IP, error) { + if strings.Contains(transportConfig, "|") { + return nil, errors.New("multi-part config is not supported") + } + if transportConfig = strings.TrimSpace(transportConfig); transportConfig == "" { + return nil, errors.New("config is required") + } + url, err := url.Parse(transportConfig) + if err != nil { + return nil, fmt.Errorf("failed to parse config: %w", err) + } + if url.Scheme != "ss" { + return nil, errors.New("config must start with 'ss://'") + } + ipList, err := net.LookupIP(url.Hostname()) + if err != nil { + return nil, fmt.Errorf("invalid server hostname: %w", err) + } + + // todo: we only tested IPv4 routing table, need to test IPv6 in the future + for _, ip := range ipList { + if ip = ip.To4(); ip != nil { + return ip, nil + } + } + return nil, errors.New("IPv6 only Shadowsocks server is not supported yet") +} diff --git a/x/examples/outline-cli/outline_packet_proxy.go b/x/examples/outline-cli/outline_packet_proxy.go new file mode 100644 index 00000000..386ef627 --- /dev/null +++ b/x/examples/outline-cli/outline_packet_proxy.go @@ -0,0 +1,67 @@ +// Copyright 2023 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 main + +import ( + "context" + "fmt" + "log" + + "github.com/Jigsaw-Code/outline-sdk/network" + "github.com/Jigsaw-Code/outline-sdk/network/dnstruncate" + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/x/config" + "github.com/Jigsaw-Code/outline-sdk/x/connectivity" +) + +type outlinePacketProxy struct { + network.DelegatePacketProxy + + remote, fallback network.PacketProxy + remotePl transport.PacketListener +} + +func newOutlinePacketProxy(transportConfig string) (opp *outlinePacketProxy, err error) { + opp = &outlinePacketProxy{} + + if opp.remotePl, err = config.NewShadowsocksPacketListenerFromPart(transportConfig); err != nil { + return nil, fmt.Errorf("failed to create UDP packet listener: %w", err) + } + if opp.remote, err = network.NewPacketProxyFromPacketListener(opp.remotePl); err != nil { + return nil, fmt.Errorf("failed to create UDP packet proxy: %w", err) + } + if opp.fallback, err = dnstruncate.NewPacketProxy(); err != nil { + return nil, fmt.Errorf("failed to create DNS truncate packet proxy: %w", err) + } + if opp.DelegatePacketProxy, err = network.NewDelegatePacketProxy(opp.fallback); err != nil { + return nil, fmt.Errorf("failed to create delegate UDP proxy: %w", err) + } + + return +} + +func (proxy *outlinePacketProxy) testConnectivityAndRefresh(resolver, domain string) error { + dialer := transport.PacketListenerDialer{Listener: proxy.remotePl} + dnsResolver := &transport.PacketDialerEndpoint{Dialer: dialer, Address: resolver} + _, err := connectivity.TestResolverPacketConnectivity(context.Background(), dnsResolver, domain) + + if err != nil { + log.Println("[info] remote server cannot handle UDP traffic, switch to DNS truncate mode") + return proxy.SetProxy(proxy.fallback) + } else { + log.Println("[info] remote server supports UDP, we will delegate all UDP packets to it") + return proxy.SetProxy(proxy.remote) + } +} diff --git a/x/examples/outline-cli/tun_device.go b/x/examples/outline-cli/tun_device.go index d04bc399..74f7d183 100644 --- a/x/examples/outline-cli/tun_device.go +++ b/x/examples/outline-cli/tun_device.go @@ -15,7 +15,7 @@ package main import ( - "github.com/Jigsaw-Code/outline-internal-sdk/network" + "github.com/Jigsaw-Code/outline-sdk/network" ) type TunDevice interface {