Skip to content

Commit

Permalink
Fix terminal
Browse files Browse the repository at this point in the history
  • Loading branch information
blechschmidt committed Nov 13, 2023
1 parent 1ece8f3 commit 502e413
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 75 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,8 @@ These features are currently considered experimental.
## Security
First of all, **do not set the SUID bit on pallium binaries**. Pallium is not meant to be a SUID executable.

For some operations, pallium needs
to be run as root (e.g. using `sudo`). When it is run as root, to prevent configuration files from being tampered with,
pallium requires configuration files to be owned by root and to be inaccessible to other users.
You are responsible for keeping secrets inside pallium configuration files safe by ensuring that only authorized users
may read the files.

Pallium will not magically anonymize your traffic. Applications and virtual machines routing their traffic through the
configured cascade may expose sensitive information, such as installation identifiers or hardware fingerprints. In
Expand Down
2 changes: 2 additions & 0 deletions dist/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ test "$INSTALL" = "1" && {
set -e
wget -c https://www.python.org/ftp/python/3.10.13/Python-3.10.13.tar.xz
tar -Jxf Python-3.10.13.tar.xz

# We don't use TLS/SSL anywhere, we just need to get Python to build, which is easier with OpenSSL.
wget -c https://www.openssl.org/source/openssl-1.1.1o.tar.gz
tar -xf openssl-1.1.1o.tar.gz
cd openssl-1.1.1o || exit 1
Expand Down
147 changes: 98 additions & 49 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,118 @@
# This script will attempt to automatically detect the package manager, perform a system upgrade and install the
# required dependencies.

echo 'Pallium install script'
# Base URL for binaries. Must not end with a slash.
BASE_URL="https://github.com/blechschmidt/pallium/releases/latest/download"

OPTS=$(getopt -o '' --long "dependencies-only,noconfirm,test-dependencies,no-dependencies" -- "$@")
echo 'Pallium Installation Script'

OPTS=$(getopt -o '' --long "dependencies-only,noconfirm,test-dependencies,no-dependencies,binary" -- "$@")

eval set -- "$OPTS"

CONFIRM=1
FROM_SOURCE=1

while true; do
case "$1" in
--dependencies-only) DEPENDENCIES_ONLY=1; shift;;
--no-dependencies) NO_DEPENDENCIES=1; shift;;
--test-dependencies) TEST_DEPENDENCIES=1; shift;; # Whether to install dependencies required for tests.
--test-dependencies) TEST_DEPENDENCIES=1; shift;; # Install dependencies required for tests. Not in use yet.
--noconfirm) CONFIRM=0; shift;;
--binary) FROM_SOURCE=0; shift;; # Whether to install from source
--) shift; break;;
*) echo "Error."; exit 1;;
esac
done

contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
# shellcheck disable=SC2039
is_root() { [ "${EUID:-$(id -u)}" -eq 0 ]; }

get_goarch() {
# https://github.com/golang/go/blob/016d7552138077741a9c3fdadc73c0179f5d3ff7/src/cmd/dist/main.go#L94
OUT=$(uname -m)
OUT_ALL=$(uname -r)
export RESULT

if contains "$OUT_ALL" "RELEASE_ARM64"; then
RESULT="arm64"
elif contains "$OUT" "x86_64" || contains "$OUT" "amd64"; then
RESULT="amd64"
elif contains "$OUT" "86"; then
RESULT="386"
# Darwin case ignored
elif contains "$OUT" "aarch64" || contains "$OUT" "arm64"; then
RESULT="arm64"
elif contains "$OUT" "arm"; then
RESULT="arm"
# NetBSD case ignored
elif contains "$OUT" "ppc64le"; then
RESULT="ppc64le"
elif contains "$OUT" "ppc64"; then
RESULT="ppc64"
elif contains "$OUT" "mips64"; then
RESULT="mips64"
LE=$(python3 -c "import sys;sys.exit(int(sys.byteorder=='little'))")
test "$LE" = "1" && RESULT="mips64le"
elif contains "$OUT" "mips"; then
RESULT="mips"
LE=$(python3 -c "import sys;sys.exit(int(sys.byteorder=='little'))")
test "$LE" = "1" && RESULT="mipsle"
elif contains "$OUT" "loongarch64"; then
RESULT="loong64"
elif contains "$OUT" "riscv64"; then
RESULT="riscv64"
elif contains "$OUT" "s390x"; then
RESULT="s390x"
fi
}

download_file() {
# This breaks if the arguments contain quotes
SRC="$1"
DST="$2"
if command -v wget >/dev/null 2>&1; then
wget -O- "$SRC" > "$DST"
elif command -v curl >/dev/null 2>&1; then
curl "$SRC" > "$DST"
else
echo "curl or wget are required. Aborting."
exit 1
fi
}

install_binary() {
get_goarch
ARCH="$RESULT"
DOWNLOAD_URL="$BASE_URL/pallium-x86_64-bundle-linux"
DST_DIR=~/".local/share/applications"
is_root && {
DST_DIR="/usr/local/bin"
}
DST_FILENAME="pallium"

# If stdout is a terminal.
test -t 1 && {
printf "Installation directory [Default: %s]: " "$DST_DIR" >&2
read -r OVERRIDDEN_DST_DIR
}

test -z "$OVERRIDDEN_DST_DIR" || {
DST_DIR="$OVERRIDDEN_DST_DIR"
}

DST_PATH="$DST_DIR/$DST_FILENAME"
mkdir -p "$DST_DIR"
download_file "$DOWNLOAD_URL" "$DST_PATH"
chmod a+x "$DST_PATH"
}

test "$FROM_SOURCE" != "1" && {
install_binary
exit 0
}

test "$(id -u)" -eq 0 || {
echo "You must be root."
exit 1
Expand Down Expand Up @@ -153,49 +246,6 @@ install_unzip() {
test $? -eq 0 || install_pkg unzip
}

contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }

get_goarch() {
# https://github.com/golang/go/blob/016d7552138077741a9c3fdadc73c0179f5d3ff7/src/cmd/dist/main.go#L94
OUT=$(uname -m)
OUT_ALL=$(uname -r)
export RESULT

if contains "$OUT_ALL" "RELEASE_ARM64"; then
RESULT="arm64"
elif contains "$OUT" "x86_64" || contains "$OUT" "amd64"; then
RESULT="amd64"
elif contains "$OUT" "86"; then
RESULT="386"
# Darwin case ignored
elif contains "$OUT" "aarch64" || contains "$OUT" "arm64"; then
RESULT="arm64"
elif contains "$OUT" "arm"; then
RESULT="arm"
# NetBSD case ignored
elif contains "$OUT" "ppc64le"; then
RESULT="ppc64le"
elif contains "$OUT" "ppc64"; then
RESULT="ppc64"
elif contains "$OUT" "mips64"; then
RESULT="mips64"
LE=$(python3 -c "import sys;sys.exit(int(sys.byteorder=='little'))")
test "$LE" = "1" && RESULT="mips64le"
elif contains "$OUT" "mips"; then
RESULT="mips"
LE=$(python3 -c "import sys;sys.exit(int(sys.byteorder=='little'))")
test "$LE" = "1" && RESULT="mipsle"
elif contains "$OUT" "loongarch64"; then
RESULT="loong64"
elif contains "$OUT" "riscv64"; then
RESULT="riscv64"
elif contains "$OUT" "s390x"; then
RESULT="s390x"
fi


}

install_curl() {
command -v curl >/dev/null 2>&1
test $? -eq 0 || install_pkg curl
Expand All @@ -216,9 +266,8 @@ install_tun2socks() {

install_curl

VERSION=$(curl -Ls -o /dev/null -w '%{url_effective}' https://github.com/xjasonlyu/tun2socks/releases/latest | cut -d / -f 8)
TMP=$(mktemp -d)
URL=https://github.com/xjasonlyu/tun2socks/releases/download/"$VERSION"/tun2socks-linux-"$SUFFIX".zip
URL=https://github.com/xjasonlyu/tun2socks/releases/latest/download/tun2socks-linux-"$SUFFIX".zip
ask_continue "$URL will be downloaded and extracted to /usr/local/bin/."
curl -L "$URL" >"$TMP/tun2socks.zip"
install_unzip
Expand Down Expand Up @@ -262,4 +311,4 @@ test "$NO_DEPENDENCIES" != "1" && {
install_tun2socks
install_slirp4netns
install_gvisor
}
}
48 changes: 37 additions & 11 deletions pallium/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,27 @@ def profile_from_args(args):
return profile


def get_tty(stdin_only=True):
streams = [sys.stdin]
if not stdin_only:
streams += [sys.stdout, sys.stderr]
ttyname = None
def is_tty(fileno):
try:
os.ttyname(fileno)
return True
except OSError as e:
if e.errno != errno.ENOTTY:
raise
return False


def get_tty(exclude_stdin):
streams = [sys.stdout, sys.stderr]
if not exclude_stdin:
streams += [sys.stdin]

for s in streams:
try:
ttyname = os.ttyname(s.fileno())
return s.fileno()
except OSError as e:
if e.errno != errno.ENOTTY:
raise
return ttyname


def wait_sigint():
Expand All @@ -74,7 +83,8 @@ def pallium_run(args):
stdin=sys.stdin
)

session.run(profile.command, terminal=get_tty() is not None, call_args=call_args)
terminal = is_tty(sys.stdin.fileno()) and is_tty(sys.stdout.fileno()) and is_tty(sys.stderr.fileno())
session.run(profile.command, terminal=terminal, call_args=call_args)

global no_interrupt
no_interrupt = True
Expand Down Expand Up @@ -168,19 +178,33 @@ def pallium_exec(args):
print('Command required.', file=sys.stderr)
sys.exit(1)

try:
config_json = json.loads(args.config)
except json.JSONDecodeError:
config_json = None
if not isinstance(config_json, dict):
config_json = None

if 'config' not in args or args.config is None or args.config == '-':
logging.debug("Reading profile from stdin")
data = json.loads(sys.stdin.read())

# Reconnect stdin to parent terminal
# If stdout or stderr are ttys, one of them will be used for stdin
tty = get_tty(False)
tty = get_tty(True)
if tty is not None:
logging.debug("Reconnect stdin to parent terminal")
sys.stdin = open(tty)

# Open the tty at stdin FD 0
def opener(_, __):
return os.dup2(tty, 0)
sys.stdin = open("", opener=opener)

profile = Profile.from_config(data)
new_session = True
elif config_json:
profile = Profile.from_config(config_json)
new_session = True
else:
profile = profile_from_args(args)
new_session = False
Expand All @@ -199,7 +223,9 @@ def pallium_exec(args):
shell=isinstance(profile.command, str),
stdin=sys.stdin
)
sys.exit(session.run(command, terminal=get_tty() is not None, ns_index=args.namespace, root=args.root,

terminal = is_tty(sys.stdin.fileno()) and is_tty(sys.stdout.fileno()) and is_tty(sys.stderr.fileno())
sys.exit(session.run(command, terminal=terminal, ns_index=args.namespace, root=args.root,
call_args=call_args))


Expand Down
12 changes: 0 additions & 12 deletions pallium/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,18 +417,6 @@ def from_file(cls, filepath) -> 'Profile':
@param filepath: The full path to the configuration file.
@return: The profile constructed according to the configuration file.
"""
# Prevent unauthorized users from instructing the program to execute malicious commands by modifying the
# configuration file.
try:
stat_result = os.stat(filepath)
except FileNotFoundError:
raise ProfileNotFoundError('The profile was not found. %s does not exist.' % filepath)
if not stat.S_ISREG(stat_result.st_mode):
raise ProfileNotFoundError('The profile was not found. %s is not a regular file.' % filepath)
if not util.secure_config(filepath, 0o600) and security.is_sudo_or_root():
raise ConfigurationError('The mode of the configuration file should be 600 (rw-------) or more restrictive '
'and it should be owned by root for security reasons.')

with open(filepath, 'r') as f:
settings = json.loads(f.read())

Expand Down

0 comments on commit 502e413

Please sign in to comment.