#!/bin/sh
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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
#
#     http://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.

set -eu

# gost's allowlist is never written here: the sidecar entrypoint empties it at
# start and this policy only ever forwards through the audited proxy, so gost
# forwards nothing on its own. That also removes the packet-mark exemption gost
# needs when it does forward — the sidecar shares the subject's network
# namespace, so a mark the sidecar can set is one the subject can set too.
GOST_PORT=12345
NFTABLES_RULESET_NAME=gost_egress
PROXY_HOST=maka-eval-mitmproxy
PROXY_PORT=8080

remove_nftables() {
  nft delete table inet "$NFTABLES_RULESET_NAME" 2>/dev/null || true
}

PROXY_IPV4_PATH=/opt/maka-egress/proxy-ipv4

resolve_proxy_ip() {
  # The proxy publishes this file before it is healthy. Reading it does not
  # depend on Docker DNS, so deny-all then allow still has an address.
  if [ ! -s "$PROXY_IPV4_PATH" ]; then
    return
  fi
  ip=$(tr -d ' \t\r\n' < "$PROXY_IPV4_PATH")
  case "$ip" in
    "" | *.*.*.*.* | *[!0-9.]*)
      return
      ;;
  esac
  # Same four-octet program as relay_agent.IPV4_OCTET_AWK.
  if ! printf '%s\n' "$ip" | awk '
BEGIN { FS = "." }
NF != 4 { exit 1 }
{
  for (i = 1; i <= 4; i++) {
    if ($i !~ /^(0|[1-9][0-9]*)$/ || $i + 0 > 255) exit 1
  }
}
'; then
    return
  fi
  printf '%s\n' "$ip"
}

# The only difference between the two modes this policy installs is whether the
# audited proxy is exempt, so they are one ruleset with one optional pair of
# rules rather than two rulesets that have to be kept in step.
apply_ruleset() {
  proxy_ip="${1:-}"
  nft --file - <<EOF
add table inet $NFTABLES_RULESET_NAME
flush table inet $NFTABLES_RULESET_NAME

table inet $NFTABLES_RULESET_NAME {
  chain output {
    type nat hook output priority dstnat; policy accept;

    fib daddr type local return
${proxy_ip:+    ip daddr $proxy_ip tcp dport $PROXY_PORT return}
    meta l4proto tcp redirect to :$GOST_PORT
  }

  chain egress {
    type filter hook output priority filter; policy accept;

    # Docker DNATs 127.0.0.11:53 onto another local port before this hook.
    # Matching only dport 53 here misses, and the local exemption below
    # would then accept the rewritten packet.
    ip daddr 127.0.0.11 reject
    fib daddr type local accept
${proxy_ip:+    ip daddr $proxy_ip tcp dport $PROXY_PORT accept}
    # Rejecting everything else rather than only non-TCP: the nat chain above
    # rewrote every TCP destination this policy admits to a local one, so what
    # reaches here still carrying a public destination is traffic the nat hook
    # never saw — a connection conntrack established before the policy existed.
    reject
  }
}
EOF
}

setup_proxy_only() {
  proxy_ip="$(resolve_proxy_ip)"
  if [ -z "$proxy_ip" ]; then
    echo "could not resolve Eval egress proxy" >&2
    exit 1
  fi
  apply_ruleset "$proxy_ip"
  echo "ok: Eval proxy-only egress applied ($proxy_ip:$PROXY_PORT)"
}

case "${1:-}" in
  allow-all)
    remove_nftables
    echo "ok: allow all egress"
    ;;
  deny-all)
    # A phase that asked for no network gets none. Answering it with the
    # proxy-only ruleset would hand it a route this override invented, which
    # Harbor's own deny-all does not grant either.
    apply_ruleset
    echo "ok: Eval no-network egress applied"
    ;;
  allow)
    shift
    if [ "$#" -ne 1 ] || [ "$1" != "$PROXY_HOST" ]; then
      echo "Eval egress policy accepts only $PROXY_HOST" >&2
      exit 2
    fi
    setup_proxy_only
    ;;
  show)
    if ! rules="$(nft list table inet "$NFTABLES_RULESET_NAME" 2>/dev/null)"; then
      echo "mode: allow-all"
    elif printf '%s' "$rules" | grep -q "dport $PROXY_PORT"; then
      echo "mode: Eval proxy-only"
      echo "proxy: $PROXY_HOST:$PROXY_PORT"
    else
      echo "mode: Eval no-network"
    fi
    ;;
  rules)
    nft list table inet "$NFTABLES_RULESET_NAME" 2>/dev/null || echo "mode: allow-all"
    ;;
  *)
    echo "Usage: network-policy {show|allow-all|deny-all|allow|rules}" >&2
    exit 2
    ;;
esac
