Browse Source

Command line flag and config enhancements
* Command line flags for generate parameters
* Support loading multiple config files
* Rudimentary validation of config values
* Better command line usage text

Rod Hynes 10 years ago
parent
commit
b54eb0f760
4 changed files with 130 additions and 39 deletions
  1. 78 9
      Server/main.go
  2. 48 16
      psiphon/server/config.go
  3. 2 12
      psiphon/server/log.go
  4. 2 2
      psiphon/server/services.go

+ 78 - 9
Server/main.go

@@ -24,28 +24,76 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"strings"
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server"
 )
 
 func main() {
 
+	var generateServerIPaddress string
+	var generateWebServerPort, generateSSHServerPort, generateObfuscatedSSHServerPort int
+	var runConfigFilenames stringListFlag
+
+	flag.StringVar(
+		&generateServerIPaddress,
+		"ipaddress",
+		server.DEFAULT_SERVER_IP_ADDRESS,
+		"generate with this server `IP address`")
+
+	flag.IntVar(
+		&generateWebServerPort,
+		"webport",
+		server.DEFAULT_WEB_SERVER_PORT,
+		"generate with this web server `port`; 0 for no web server")
+
+	flag.IntVar(
+		&generateSSHServerPort,
+		"sshport",
+		server.DEFAULT_SSH_SERVER_PORT,
+		"generate with this SSH server `port`; 0 for no SSH server")
+
+	flag.IntVar(
+		&generateObfuscatedSSHServerPort,
+		"osshport",
+		server.DEFAULT_OBFUSCATED_SSH_SERVER_PORT,
+		"generate with this Obfuscated SSH server `port`; 0 for no Obfuscated SSH server")
+
+	flag.Var(
+		&runConfigFilenames,
+		"config",
+		"run with this config `filename`; may be repeated to load multiple config files")
+
+	flag.Usage = func() {
+		fmt.Fprintf(os.Stderr,
+			"Usage:\n\n"+
+				"%s generate    generates a configuration and server entry\n"+
+				"%s run         runs configured services\n\n",
+			os.Args[0], os.Args[0])
+		flag.PrintDefaults()
+	}
+
 	flag.Parse()
 
 	args := flag.Args()
 
-	// TODO: add working directory flag
 	configFilename := server.SERVER_CONFIG_FILENAME
+
 	serverEntryFilename := server.SERVER_ENTRY_FILENAME
 
 	if len(args) < 1 {
-		fmt.Errorf("usage: '%s generate' or '%s run'", os.Args[0])
+		flag.Usage()
 		os.Exit(1)
 	} else if args[0] == "generate" {
 
-		// TODO: flags to set generate params
 		configFileContents, serverEntryFileContents, err := server.GenerateConfig(
-			&server.GenerateConfigParams{})
+			&server.GenerateConfigParams{
+				ServerIPAddress:         generateServerIPaddress,
+				WebServerPort:           generateWebServerPort,
+				SSHServerPort:           generateSSHServerPort,
+				ObfuscatedSSHServerPort: generateObfuscatedSSHServerPort,
+			})
+
 		if err != nil {
 			fmt.Errorf("generate failed: %s", err)
 			os.Exit(1)
@@ -64,16 +112,37 @@ func main() {
 
 	} else if args[0] == "run" {
 
-		configFileContents, err := ioutil.ReadFile(configFilename)
-		if err != nil {
-			fmt.Errorf("error loading configuration file: %s", err)
-			os.Exit(1)
+		if len(runConfigFilenames) == 0 {
+			runConfigFilenames = []string{configFilename}
+		}
+
+		var configFileContents [][]byte
+
+		for _, configFilename := range runConfigFilenames {
+			ccontents, err := ioutil.ReadFile(configFilename)
+			if err != nil {
+				fmt.Errorf("error loading configuration file: %s", err)
+				os.Exit(1)
+			}
+
+			configFileContents = append(configFileContents, ccontents)
 		}
 
-		err = server.RunServices(configFileContents)
+		err := server.RunServices(configFileContents)
 		if err != nil {
 			fmt.Errorf("run failed: %s", err)
 			os.Exit(1)
 		}
 	}
 }
+
+type stringListFlag []string
+
+func (list *stringListFlag) String() string {
+	return strings.Join(*list, ", ")
+}
+
+func (list *stringListFlag) Set(flagValue string) error {
+	*list = append(*list, flagValue)
+	return nil
+}

+ 48 - 16
psiphon/server/config.go

@@ -26,6 +26,7 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"encoding/pem"
+	"errors"
 	"fmt"
 	"math/big"
 	"strings"
@@ -88,7 +89,8 @@ type Config struct {
 	DiscoveryValueHMACKey string
 
 	// GeoIPDatabaseFilename is the path of the GeoIP2/GeoLite2
-	// MaxMind database file.
+	// MaxMind database file. when blank, no GeoIP lookups are
+	// performed.
 	GeoIPDatabaseFilename string
 
 	// ServerIPAddress is the public IP address of the server.
@@ -110,6 +112,10 @@ type Config struct {
 	// authenticate itself to clients.
 	WebServerPrivateKey string
 
+	// SSHServerPort is the listening port of the SSH server.
+	// When <= 0, no SSH server component is run.
+	SSHServerPort int
+
 	// SSHPrivateKey is the SSH host key. The same key is used for
 	// both the SSH and Obfuscated SSH servers.
 	SSHPrivateKey string
@@ -129,18 +135,14 @@ type Config struct {
 	// and Obfuscated SSH servers.
 	SSHPassword string
 
-	// SSHServerPort is the listening port of the SSH server.
-	// When <= 0, no SSH server component is run.
-	SSHServerPort int
+	// ObfuscatedSSHServerPort is the listening port of the Obfuscated SSH server.
+	// When <= 0, no Obfuscated SSH server component is run.
+	ObfuscatedSSHServerPort int
 
 	// ObfuscatedSSHKey is the secret key for use in the Obfuscated
 	// SSH protocol.
 	ObfuscatedSSHKey string
 
-	// ObfuscatedSSHServerPort is the listening port of the Obfuscated SSH server.
-	// When <= 0, no Obfuscated SSH server component is run.
-	ObfuscatedSSHServerPort int
-
 	// RedisServerAddress is the TCP address of a redis server. When
 	// set, redis is used to store per-session GeoIP information.
 	RedisServerAddress string
@@ -167,17 +169,47 @@ func (config *Config) UseRedis() bool {
 	return config.RedisServerAddress != ""
 }
 
-// LoadConfig loads and validates a JSON encoded server config.
-func LoadConfig(configJson []byte) (*Config, error) {
+// LoadConfig loads and validates a JSON encoded server config. If more than one
+// JSON config is specified, then all are loaded and values are merged together,
+// in order. Multiple configs allows for use cases like storing static, server-specific
+// values in a base config while also deploying network-wide throttling settings
+// in a secondary file that can be paved over on all server hosts.
+func LoadConfig(configJSONs [][]byte) (*Config, error) {
 
+	// Note: default values are set in GenerateConfig
 	var config Config
-	err := json.Unmarshal(configJson, &config)
-	if err != nil {
-		return nil, psiphon.ContextError(err)
+
+	for _, configJSON := range configJSONs {
+		err := json.Unmarshal(configJSON, &config)
+		if err != nil {
+			return nil, psiphon.ContextError(err)
+		}
 	}
 
-	// TODO: config field validation
-	// TODO: validation case: OSSH requires extra fields
+	if config.ServerIPAddress == "" {
+		return nil, errors.New("server IP address is missing from config file")
+	}
+
+	if config.WebServerPort > 0 && (config.WebServerSecret == "" || config.WebServerCertificate == "" ||
+		config.WebServerPrivateKey == "") {
+
+		return nil, errors.New(
+			"web server requires WebServerSecret, WebServerCertificate, WebServerPrivateKey")
+	}
+
+	if config.SSHServerPort > 0 && (config.SSHPrivateKey == "" || config.SSHServerVersion == "" ||
+		config.SSHUserName == "" || config.SSHPassword == "") {
+
+		return nil, errors.New(
+			"SSH server requires SSHPrivateKey, SSHServerVersion, SSHUserName, SSHPassword")
+	}
+
+	if config.ObfuscatedSSHServerPort > 0 && (config.SSHPrivateKey == "" || config.SSHServerVersion == "" ||
+		config.SSHUserName == "" || config.SSHPassword == "" || config.ObfuscatedSSHKey == "") {
+
+		return nil, errors.New(
+			"Obfuscated SSH server requires SSHPrivateKey, SSHServerVersion, SSHUserName, SSHPassword, ObfuscatedSSHKey")
+	}
 
 	return &config, nil
 }
@@ -293,7 +325,7 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) {
 		LogLevel:                DEFAULT_LOG_LEVEL,
 		SyslogAddress:           "",
 		SyslogFacility:          "",
-		SyslogTag:               "",
+		SyslogTag:               DEFAULT_SYSLOG_TAG,
 		DiscoveryValueHMACKey:   "",
 		GeoIPDatabaseFilename:   DEFAULT_GEO_IP_DATABASE_FILENAME,
 		ServerIPAddress:         serverIPaddress,

+ 2 - 12
psiphon/server/log.go

@@ -77,12 +77,7 @@ var log *ContextLogger
 // goroutine.
 func InitLogging(config *Config) error {
 
-	logLevel := DEFAULT_LOG_LEVEL
-	if config.LogLevel != "" {
-		logLevel = config.LogLevel
-	}
-
-	level, err := logrus.ParseLevel(logLevel)
+	level, err := logrus.ParseLevel(config.LogLevel)
 	if err != nil {
 		return psiphon.ContextError(err)
 	}
@@ -93,16 +88,11 @@ func InitLogging(config *Config) error {
 
 	if config.SyslogAddress != "" {
 
-		tag := DEFAULT_SYSLOG_TAG
-		if config.SyslogTag != "" {
-			tag = config.SyslogTag
-		}
-
 		syslogHook, err = logrus_syslog.NewSyslogHook(
 			"udp",
 			config.SyslogAddress,
 			getSyslogPriority(config),
-			tag)
+			config.SyslogTag)
 
 		if err != nil {
 			return psiphon.ContextError(err)

+ 2 - 2
psiphon/server/services.go

@@ -35,9 +35,9 @@ import (
 // redis connection pooling; and then starts the server components and runs them
 // until os.Interrupt or os.Kill signals are received. The config determines
 // which components are run.
-func RunServices(encodedConfig []byte) error {
+func RunServices(encodedConfigs [][]byte) error {
 
-	config, err := LoadConfig(encodedConfig)
+	config, err := LoadConfig(encodedConfigs)
 	if err != nil {
 		log.WithContextFields(LogFields{"error": err}).Error("load config failed")
 		return psiphon.ContextError(err)