|
|
@@ -0,0 +1,274 @@
|
|
|
+/*
|
|
|
+ * Copyright (c) 2022, Psiphon Inc.
|
|
|
+ * All rights reserved.
|
|
|
+ *
|
|
|
+ * This program is free software: you can redistribute it and/or modify
|
|
|
+ * it under the terms of the GNU General Public License as published by
|
|
|
+ * the Free Software Foundation, either version 3 of the License, or
|
|
|
+ * (at your option) any later version.
|
|
|
+ *
|
|
|
+ * This program is distributed in the hope that it will be useful,
|
|
|
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+ * GNU General Public License for more details.
|
|
|
+ *
|
|
|
+ * You should have received a copy of the GNU General Public License
|
|
|
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
+ *
|
|
|
+ */
|
|
|
+
|
|
|
+package server
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/json"
|
|
|
+ "io/ioutil"
|
|
|
+ "os"
|
|
|
+ "reflect"
|
|
|
+ "testing"
|
|
|
+
|
|
|
+ "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
|
|
|
+)
|
|
|
+
|
|
|
+func TestTrafficRulesFilters(t *testing.T) {
|
|
|
+
|
|
|
+ trafficRulesJSON := `
|
|
|
+ {
|
|
|
+ "DefaultRules" : {
|
|
|
+ "RateLimits" : {
|
|
|
+ "WriteUnthrottledBytes": 1,
|
|
|
+ "WriteBytesPerSecond": 2,
|
|
|
+ "ReadUnthrottledBytes": 3,
|
|
|
+ "ReadBytesPerSecond": 4,
|
|
|
+ "UnthrottleFirstTunnelOnly": true
|
|
|
+ },
|
|
|
+ "AllowTCPPorts" : [5],
|
|
|
+ "AllowUDPPorts" : [6]
|
|
|
+ },
|
|
|
+
|
|
|
+ "FilteredRules" : [
|
|
|
+
|
|
|
+ {
|
|
|
+ "Filter" : {
|
|
|
+ "Regions" : ["R2"],
|
|
|
+ "HandshakeParameters" : {
|
|
|
+ "client_version" : ["1"]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "Rules" : {
|
|
|
+ "RateLimits" : {
|
|
|
+ "WriteBytesPerSecond": 7,
|
|
|
+ "ReadBytesPerSecond": 8
|
|
|
+ },
|
|
|
+ "AllowTCPPorts" : [5,9],
|
|
|
+ "AllowUDPPorts" : [6,10]
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ "Filter" : {
|
|
|
+ "TunnelProtocols" : ["P2"],
|
|
|
+ "Regions" : ["R3", "R4"],
|
|
|
+ "HandshakeParameters" : {
|
|
|
+ "client_version" : ["1", "2"]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "ExceptFilter" : {
|
|
|
+ "ISPs" : ["I2", "I3"],
|
|
|
+ "HandshakeParameters" : {
|
|
|
+ "client_version" : ["1"]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "Rules" : {
|
|
|
+ "RateLimits" : {
|
|
|
+ "WriteBytesPerSecond": 11,
|
|
|
+ "ReadBytesPerSecond": 12
|
|
|
+ },
|
|
|
+ "AllowTCPPorts" : [5,13],
|
|
|
+ "AllowUDPPorts" : [6,14]
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ "Filter" : {
|
|
|
+ "Regions" : ["R3", "R4"],
|
|
|
+ "HandshakeParameters" : {
|
|
|
+ "client_version" : ["1", "2"]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "ExceptFilter" : {
|
|
|
+ "ISPs" : ["I2", "I3"],
|
|
|
+ "HandshakeParameters" : {
|
|
|
+ "client_version" : ["1"]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "Rules" : {
|
|
|
+ "RateLimits" : {
|
|
|
+ "WriteBytesPerSecond": 15,
|
|
|
+ "ReadBytesPerSecond": 16
|
|
|
+ },
|
|
|
+ "AllowTCPPorts" : [5,17],
|
|
|
+ "AllowUDPPorts" : [6,18]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ `
|
|
|
+
|
|
|
+ file, err := ioutil.TempFile("", "trafficRules.config")
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("TempFile create failed: %s", err)
|
|
|
+ }
|
|
|
+ _, err = file.Write([]byte(trafficRulesJSON))
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("TempFile write failed: %s", err)
|
|
|
+ }
|
|
|
+ file.Close()
|
|
|
+ configFileName := file.Name()
|
|
|
+ defer os.Remove(configFileName)
|
|
|
+
|
|
|
+ trafficRules, err := NewTrafficRulesSet(configFileName)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("NewTrafficRulesSet failed: %s", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ err = trafficRules.Validate()
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("TrafficRulesSet.Validate failed: %s", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ makePortList := func(portsJSON string) common.PortList {
|
|
|
+ var p common.PortList
|
|
|
+ _ = json.Unmarshal([]byte(portsJSON), &p)
|
|
|
+ return p
|
|
|
+ }
|
|
|
+
|
|
|
+ testCases := []struct {
|
|
|
+ description string
|
|
|
+ isFirstTunnelInSession bool
|
|
|
+ tunnelProtocol string
|
|
|
+ geoIPData GeoIPData
|
|
|
+ state handshakeState
|
|
|
+ expectedWriteUnthrottledBytes int64
|
|
|
+ expectedWriteBytesPerSecond int64
|
|
|
+ expectedReadUnthrottledBytes int64
|
|
|
+ expectedReadBytesPerSecond int64
|
|
|
+ expectedAllowTCPPorts common.PortList
|
|
|
+ expectedAllowUDPPorts common.PortList
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ "get defaults",
|
|
|
+ true,
|
|
|
+ "P1",
|
|
|
+ GeoIPData{Country: "R1", ISP: "I1"},
|
|
|
+ handshakeState{apiParams: map[string]interface{}{"client_version": "1"}, completed: true},
|
|
|
+ 1, 2, 3, 4, makePortList("[5]"), makePortList("[6]"),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ "get defaults for not first tunnel in session",
|
|
|
+ false,
|
|
|
+ "P1",
|
|
|
+ GeoIPData{Country: "R1", ISP: "I1"},
|
|
|
+ handshakeState{apiParams: map[string]interface{}{"client_version": "1"}, completed: true},
|
|
|
+ 0, 2, 0, 4, makePortList("[5]"), makePortList("[6]"),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ "get first filtered rule",
|
|
|
+ true,
|
|
|
+ "P1",
|
|
|
+ GeoIPData{Country: "R2", ISP: "I1"},
|
|
|
+ handshakeState{apiParams: map[string]interface{}{"client_version": "1"}, completed: true},
|
|
|
+ 1, 7, 3, 8, makePortList("[5,9]"), makePortList("[6,10]"),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ "don't get first filtered rule with incomplete match",
|
|
|
+ true,
|
|
|
+ "P1",
|
|
|
+ GeoIPData{Country: "R2", ISP: "I1"},
|
|
|
+ handshakeState{apiParams: map[string]interface{}{"client_version": "2"}, completed: true},
|
|
|
+ 1, 2, 3, 4, makePortList("[5]"), makePortList("[6]"),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ "get second filtered rule",
|
|
|
+ true,
|
|
|
+ "P2",
|
|
|
+ GeoIPData{Country: "R3", ISP: "I1"},
|
|
|
+ handshakeState{apiParams: map[string]interface{}{"client_version": "2"}, completed: true},
|
|
|
+ 1, 11, 3, 12, makePortList("[5,13]"), makePortList("[6,14]"),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ "get second filtered rule with incomplete exception",
|
|
|
+ true,
|
|
|
+ "P2",
|
|
|
+ GeoIPData{Country: "R3", ISP: "I2"},
|
|
|
+ handshakeState{apiParams: map[string]interface{}{"client_version": "2"}, completed: true},
|
|
|
+ 1, 11, 3, 12, makePortList("[5,13]"), makePortList("[6,14]"),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ "don't get second filtered rule due to exception",
|
|
|
+ true,
|
|
|
+ "P2",
|
|
|
+ GeoIPData{Country: "R3", ISP: "I2"},
|
|
|
+ handshakeState{apiParams: map[string]interface{}{"client_version": "1"}, completed: true},
|
|
|
+ 1, 2, 3, 4, makePortList("[5]"), makePortList("[6]"),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ "get third filtered rule",
|
|
|
+ true,
|
|
|
+ "P1",
|
|
|
+ GeoIPData{Country: "R3", ISP: "I1"},
|
|
|
+ handshakeState{apiParams: map[string]interface{}{"client_version": "1"}, completed: true},
|
|
|
+ 1, 15, 3, 16, makePortList("[5,17]"), makePortList("[6,18]"),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ "don't get third filtered rule due to exception",
|
|
|
+ true,
|
|
|
+ "P1",
|
|
|
+ GeoIPData{Country: "R3", ISP: "I2"},
|
|
|
+ handshakeState{apiParams: map[string]interface{}{"client_version": "1"}, completed: true},
|
|
|
+ 1, 2, 3, 4, makePortList("[5]"), makePortList("[6]"),
|
|
|
+ },
|
|
|
+ }
|
|
|
+ for _, testCase := range testCases {
|
|
|
+ t.Run(testCase.description, func(t *testing.T) {
|
|
|
+
|
|
|
+ rules := trafficRules.GetTrafficRules(
|
|
|
+ testCase.isFirstTunnelInSession,
|
|
|
+ testCase.tunnelProtocol,
|
|
|
+ testCase.geoIPData,
|
|
|
+ testCase.state)
|
|
|
+
|
|
|
+ if *rules.RateLimits.WriteUnthrottledBytes != testCase.expectedWriteUnthrottledBytes {
|
|
|
+ t.Errorf("unexpected rules.RateLimits.WriteUnthrottledBytes: %v != %v",
|
|
|
+ *rules.RateLimits.WriteUnthrottledBytes, testCase.expectedWriteUnthrottledBytes)
|
|
|
+ }
|
|
|
+ if *rules.RateLimits.WriteBytesPerSecond != testCase.expectedWriteBytesPerSecond {
|
|
|
+ t.Errorf("unexpected rules.RateLimits.WriteBytesPerSecond: %v != %v",
|
|
|
+ *rules.RateLimits.WriteBytesPerSecond, testCase.expectedWriteBytesPerSecond)
|
|
|
+ }
|
|
|
+ if *rules.RateLimits.ReadUnthrottledBytes != testCase.expectedReadUnthrottledBytes {
|
|
|
+ t.Errorf("unexpected rules.RateLimits.ReadUnthrottledBytes: %v != %v",
|
|
|
+ *rules.RateLimits.ReadUnthrottledBytes, testCase.expectedReadUnthrottledBytes)
|
|
|
+ }
|
|
|
+ if *rules.RateLimits.ReadBytesPerSecond != testCase.expectedReadBytesPerSecond {
|
|
|
+ t.Errorf("unexpected rules.RateLimits.ReadBytesPerSecond: %v != %v",
|
|
|
+ *rules.RateLimits.ReadBytesPerSecond, testCase.expectedReadBytesPerSecond)
|
|
|
+ }
|
|
|
+ if !reflect.DeepEqual(*rules.AllowTCPPorts, testCase.expectedAllowTCPPorts) {
|
|
|
+ t.Errorf("unexpected rules.RateLimits.AllowTCPPorts: %v != %v",
|
|
|
+ *rules.AllowTCPPorts, testCase.expectedAllowTCPPorts)
|
|
|
+ }
|
|
|
+ if !reflect.DeepEqual(*rules.AllowUDPPorts, testCase.expectedAllowUDPPorts) {
|
|
|
+ t.Errorf("unexpected rules.RateLimits.AllowUDPPorts: %v != %v",
|
|
|
+ *rules.AllowUDPPorts, testCase.expectedAllowUDPPorts)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|