fix: resolve clippy errors, add frontend CI steps and integration test

- Fix 10 clippy errors: type_complexity (add UserListItem/UserGroupTuple
  type aliases) and collapsible_if (collapse nested if-let chains)
- Add frontend type check (tsc --noEmit) and build (next build) to CI
- Move integration test script into repo at tests/integration_test.sh
  with hardcoded password removed (uses direct sudo instead)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
DaLaw2 2026-03-22 19:06:13 +08:00
parent 234f98cbfb
commit fd004544b9
7 changed files with 352 additions and 23 deletions

View File

@ -72,6 +72,14 @@ jobs:
ls -la "$HOME/.cargo/bin/bpf-linker"
timeout-minutes: 45
- name: Frontend type check
run: npx tsc --noEmit
working-directory: net-guardia-frontend
- name: Frontend build
run: npx next build
working-directory: net-guardia-frontend
- name: cargo check
run: cargo check --package net-guardia

View File

@ -177,10 +177,10 @@ async fn register(
Ok(new_user_id) => {
// Auto-assign to default group based on role
let default_group_name = if reg.role == "admin" { "Administrator" } else { "Viewer" };
if let Ok(groups) = db.list_user_groups() {
if let Some((group_id, _, _, _, _)) = groups.into_iter().find(|(_, name, _, _, _)| name == default_group_name) {
let _ = db.set_user_groups(new_user_id, &[group_id]);
}
if let Ok(groups) = db.list_user_groups()
&& let Some((group_id, _, _, _, _)) = groups.into_iter().find(|(_, name, _, _, _)| name == default_group_name)
{
let _ = db.set_user_groups(new_user_id, &[group_id]);
}
HttpResponse::Created()
.json(serde_json::json!({"username": reg.username, "role": reg.role}))

View File

@ -330,7 +330,7 @@ impl Database {
Ok(conn.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))?)
}
pub fn list_users(&self) -> Result<Vec<(i64, String, String, bool, String)>, Error> {
pub fn list_users(&self) -> Result<Vec<crate::interface::port::repository::UserListItem>, Error> {
let conn = self.conn.lock();
let mut stmt = conn.prepare("SELECT id, username, role, force_password_change, created_at FROM users ORDER BY id")?;
let rows = stmt.query_map([], |row| {
@ -386,7 +386,7 @@ impl Database {
}
// --- User Groups ---
pub fn list_user_groups(&self) -> Result<Vec<(i64, String, String, String, String)>, Error> {
pub fn list_user_groups(&self) -> Result<Vec<crate::interface::port::repository::UserGroupTuple>, Error> {
let conn = self.conn.lock();
let mut stmt = conn.prepare("SELECT id, name, description, permissions, created_at FROM user_groups ORDER BY id")?;
let rows = stmt.query_map([], |row| {
@ -436,7 +436,7 @@ impl Database {
Ok(affected > 0)
}
pub fn get_user_group(&self, id: i64) -> Result<Option<(i64, String, String, String, String)>, Error> {
pub fn get_user_group(&self, id: i64) -> Result<Option<crate::interface::port::repository::UserGroupTuple>, Error> {
let conn = self.conn.lock();
let result = conn.query_row(
"SELECT id, name, description, permissions, created_at FROM user_groups WHERE id = ?1",
@ -592,16 +592,16 @@ impl crate::interface::port::repository::RepositoryPort for Database {
fn insert_user(&self, username: &str, password_hash: &str, role: &str, force_password_change: bool) -> Result<i64, Error> { self.insert_user(username, password_hash, role, force_password_change) }
fn update_user_password(&self, user_id: i64, password_hash: &str) -> Result<(), Error> { self.update_user_password(user_id, password_hash) }
fn user_count(&self) -> Result<i64, Error> { self.user_count() }
fn list_users(&self) -> Result<Vec<(i64, String, String, bool, String)>, Error> { self.list_users() }
fn list_users(&self) -> Result<Vec<crate::interface::port::repository::UserListItem>, Error> { self.list_users() }
fn delete_user(&self, user_id: i64) -> Result<bool, Error> { self.delete_user(user_id) }
fn update_user_role(&self, user_id: i64, role: &str) -> Result<(), Error> { self.update_user_role(user_id, role) }
fn reset_user_password(&self, user_id: i64, password_hash: &str) -> Result<(), Error> { self.reset_user_password(user_id, password_hash) }
fn find_user_by_id(&self, user_id: i64) -> Result<Option<crate::interface::port::repository::UserTuple>, Error> { self.find_user_by_id(user_id) }
fn list_user_groups(&self) -> Result<Vec<(i64, String, String, String, String)>, Error> { self.list_user_groups() }
fn list_user_groups(&self) -> Result<Vec<crate::interface::port::repository::UserGroupTuple>, Error> { self.list_user_groups() }
fn create_user_group(&self, name: &str, description: &str, permissions: &str) -> Result<i64, Error> { self.create_user_group(name, description, permissions) }
fn update_user_group(&self, id: i64, name: &str, description: &str, permissions: &str) -> Result<(), Error> { self.update_user_group(id, name, description, permissions) }
fn delete_user_group(&self, id: i64) -> Result<bool, Error> { self.delete_user_group(id) }
fn get_user_group(&self, id: i64) -> Result<Option<(i64, String, String, String, String)>, Error> { self.get_user_group(id) }
fn get_user_group(&self, id: i64) -> Result<Option<crate::interface::port::repository::UserGroupTuple>, Error> { self.get_user_group(id) }
fn get_user_groups(&self, user_id: i64) -> Result<Vec<(i64, String, String, String)>, Error> { self.get_user_groups(user_id) }
fn set_user_groups(&self, user_id: i64, group_ids: &[i64]) -> Result<(), Error> { self.set_user_groups(user_id, group_ids) }
fn get_user_permissions(&self, user_id: i64) -> Result<Vec<String>, Error> { self.get_user_permissions(user_id) }

View File

@ -132,12 +132,12 @@ where
};
// Permission-based RBAC check
if let Some(required) = required_permission(&path, req.method()) {
if !claims.permissions.contains(&required) {
let resp = HttpResponse::Forbidden()
.json(serde_json::json!({"error": "Insufficient permissions"}));
return Ok(req.into_response(resp).map_into_right_body());
}
if let Some(required) = required_permission(&path, req.method())
&& !claims.permissions.contains(&required)
{
let resp = HttpResponse::Forbidden()
.json(serde_json::json!({"error": "Insufficient permissions"}));
return Ok(req.into_response(resp).map_into_right_body());
}
// Store claims in request extensions

View File

@ -92,10 +92,10 @@ impl ServiceFactory {
let hash = password::hash_password("admin")?;
let admin_user_id = db.insert_user("admin", &hash, "admin", true)?;
// Assign to Administrator group
if let Ok(groups) = db.list_user_groups() {
if let Some((group_id, _, _, _, _)) = groups.into_iter().find(|(_, name, _, _, _)| name == "Administrator") {
let _ = db.set_user_groups(admin_user_id, &[group_id]);
}
if let Ok(groups) = db.list_user_groups()
&& let Some((group_id, _, _, _, _)) = groups.into_iter().find(|(_, name, _, _, _)| name == "Administrator")
{
let _ = db.set_user_groups(admin_user_id, &[group_id]);
}
tracing::warn!("Default admin user created with password 'admin' — you must change it on first login");
}

View File

@ -6,6 +6,12 @@ pub type AclRuleTuple = (u8, String, String, String, u16);
/// Type alias for user record tuples: (id, username, password_hash, role, force_password_change)
pub type UserTuple = (i64, String, String, String, bool);
/// Type alias for user list items: (id, username, role, force_password_change, created_at)
pub type UserListItem = (i64, String, String, bool, String);
/// Type alias for user group tuples: (id, name, description, permissions, created_at)
pub type UserGroupTuple = (i64, String, String, String, String);
/// Port for persistent storage operations.
/// Adapters: SQLite (current), could be Postgres, etc.
pub trait RepositoryPort: Send + Sync {
@ -39,18 +45,18 @@ pub trait RepositoryPort: Send + Sync {
fn user_count(&self) -> Result<i64, Error>;
// --- User Management ---
fn list_users(&self) -> Result<Vec<(i64, String, String, bool, String)>, Error>;
fn list_users(&self) -> Result<Vec<UserListItem>, Error>;
fn delete_user(&self, user_id: i64) -> Result<bool, Error>;
fn update_user_role(&self, user_id: i64, role: &str) -> Result<(), Error>;
fn reset_user_password(&self, user_id: i64, password_hash: &str) -> Result<(), Error>;
fn find_user_by_id(&self, user_id: i64) -> Result<Option<UserTuple>, Error>;
// --- User Groups ---
fn list_user_groups(&self) -> Result<Vec<(i64, String, String, String, String)>, Error>;
fn list_user_groups(&self) -> Result<Vec<UserGroupTuple>, Error>;
fn create_user_group(&self, name: &str, description: &str, permissions: &str) -> Result<i64, Error>;
fn update_user_group(&self, id: i64, name: &str, description: &str, permissions: &str) -> Result<(), Error>;
fn delete_user_group(&self, id: i64) -> Result<bool, Error>;
fn get_user_group(&self, id: i64) -> Result<Option<(i64, String, String, String, String)>, Error>;
fn get_user_group(&self, id: i64) -> Result<Option<UserGroupTuple>, Error>;
// --- User Group Membership ---
fn get_user_groups(&self, user_id: i64) -> Result<Vec<(i64, String, String, String)>, Error>;

315
tests/integration_test.sh Executable file
View File

@ -0,0 +1,315 @@
#!/bin/bash
# NetGuardia 功能測試腳本
# 測試XDP 封包轉發、Web API、ML 引擎
# 使用 graceful shutdown所有操作設有 timeout
set -uo pipefail
TIMEOUT=10
NG_API="http://10.10.3.10:8080"
PASS=0
FAIL=0
TESTS=()
run_sudo() {
sudo "$@" 2>/dev/null
}
ng_exec() {
run_sudo podman exec netguardia bash -c "$1"
}
router_exec() {
run_sudo podman exec router bash -c "$1"
}
ext_exec() {
run_sudo podman exec external bash -c "$1"
}
int_exec() {
run_sudo podman exec internal bash -c "$1"
}
test_result() {
local name="$1"
local result="$2"
if [ "$result" -eq 0 ]; then
echo "$name"
PASS=$((PASS + 1))
else
echo "$name"
FAIL=$((FAIL + 1))
fi
TESTS+=("$name:$result")
}
echo "=========================================="
echo " NetGuardia 功能測試"
echo "=========================================="
echo ""
# ============================================================
# 1. 基礎檢查
# ============================================================
echo "--- 1. 基礎檢查 ---"
# 1a. 容器運行中
run_sudo podman ps --filter name=netguardia --format "{{.Status}}" | grep -q "Up" 2>/dev/null
test_result "netguardia 容器運行中" $?
# 1b. XDP 程式已附加
ng_exec "ip link show ng-ext 2>/dev/null | grep -q xdp"
test_result "ng-ext XDP 程式已附加" $?
ng_exec "ip link show ng-int 2>/dev/null | grep -q xdp"
test_result "ng-int XDP 程式已附加" $?
# 1c. 管理網路可達
ng_exec "ping -c1 -W $TIMEOUT 10.10.3.1 >/dev/null 2>&1"
test_result "管理網路 (10.10.3.1) 可達" $?
echo ""
# ============================================================
# 2. XDP 封包轉發測試
# ============================================================
echo "--- 2. XDP 封包轉發測試 ---"
# 2a. Router -> Internal (透過 NetGuardia)
router_exec "ping -c 3 -W $TIMEOUT 10.10.2.2 >/dev/null 2>&1"
test_result "Router → Internal ICMP 轉發 (10.10.2.2)" $?
# 2b. Internal -> Router (反向轉發)
int_exec "ping -c 3 -W $TIMEOUT 10.10.2.1 >/dev/null 2>&1"
test_result "Internal → Router ICMP 轉發 (10.10.2.1)" $?
# 2c. External -> Internal (全路徑: external -> router -> netguardia -> internal)
ext_exec "ping -c 3 -W $TIMEOUT 10.10.2.2 >/dev/null 2>&1"
test_result "External → Internal 全路徑 ICMP" $?
# 2d. Internal -> External (全路徑反向)
int_exec "ping -c 3 -W $TIMEOUT 10.10.1.2 >/dev/null 2>&1"
test_result "Internal → External 全路徑 ICMP" $?
# 2e. TCP 轉發 (HTTP)
ext_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' http://10.10.2.2/ 2>/dev/null" | grep -q "200"
test_result "External → Internal HTTP (TCP 轉發)" $?
echo ""
# ============================================================
# 3. Web API 測試
# ============================================================
echo "--- 3. Web API 測試 ---"
# 3a. Health endpoint
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/health/status" | grep -q "200"
test_result "GET /api/health/status" $?
# 3b. Health metrics
ng_exec "timeout $TIMEOUT curl -s $NG_API/api/health/metrics" | grep -q "cpu" 2>/dev/null
test_result "GET /api/health/metrics (含 CPU 資訊)" $?
# 3c. Flow stats
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/stats/flows" | grep -q "200"
test_result "GET /api/stats/flows" $?
# 3e. Stats summary
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/stats/summary" | grep -q "200"
test_result "GET /api/stats/summary" $?
# 3f. ML status
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/ml/status" | grep -q "200"
test_result "GET /api/ml/status" $?
# 3g. ACL list
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/acl/ipv4/source/whitelist" | grep -q "200"
test_result "GET /api/acl/ipv4/source/whitelist" $?
# 3h. Rate limit config
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/rate-limit/config" | grep -q "200"
test_result "GET /api/rate-limit/config" $?
echo ""
# ============================================================
# 4. ACL 功能測試
# ============================================================
echo "--- 4. ACL 功能測試 ---"
# 4a. 添加黑名單規則 (封鎖 10.10.1.5) — API 接收 SocketAddrV4 格式 "ip:port"
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X PUT '$NG_API/api/acl/ipv4/source/blacklist' -H 'Content-Type: application/json' -d '\"10.10.1.5:0\"'" | grep -q "200"
test_result "PUT ACL 黑名單規則 (封鎖 10.10.1.5)" $?
# 4b. 驗證封鎖生效 (10.10.1.5 不該能 ping 到 internal)
sleep 1
ext_exec "ping -c 2 -W 3 -I 10.10.1.5 10.10.2.2 >/dev/null 2>&1" && BLOCKED=1 || BLOCKED=0
test_result "10.10.1.5 被封鎖 (ping 失敗)" $BLOCKED
# 4c. 未封鎖的 IP 仍然可達
ext_exec "ping -c 2 -W $TIMEOUT -I 10.10.1.2 10.10.2.2 >/dev/null 2>&1"
test_result "10.10.1.2 未受影響 (ping 成功)" $?
# 4d. 移除黑名單規則
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X DELETE '$NG_API/api/acl/ipv4/source/blacklist' -H 'Content-Type: application/json' -d '\"10.10.1.5:0\"'" | grep -q "200"
test_result "DELETE ACL 黑名單規則 (解除 10.10.1.5)" $?
# 4e. 驗證解除封鎖
sleep 1
ext_exec "ping -c 2 -W $TIMEOUT -I 10.10.1.5 10.10.2.2 >/dev/null 2>&1"
test_result "10.10.1.5 解除封鎖 (ping 恢復)" $?
echo ""
# ============================================================
# 5. Rate Limit 測試
# ============================================================
echo "--- 5. Rate Limit 測試 ---"
# 5a. 讀取當前 rate limit 設定
RL_CONFIG=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/rate-limit/config")
echo "$RL_CONFIG" | grep -q "packet_rate" 2>/dev/null
test_result "Rate limit 設定可讀取" $?
echo ""
# ============================================================
# 6. ML 引擎測試
# ============================================================
echo "--- 6. ML 引擎測試 ---"
ML_STATUS=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/ml/status")
echo "$ML_STATUS" | grep -q "active\|logging\|disabled" 2>/dev/null
test_result "ML 引擎狀態可查詢" $?
# 檢查 traffic log 是否在寫入 (traffic_logging_mode = true)
ng_exec "test -f /root/NetGuardia/traffic_log.csv && wc -l < /root/NetGuardia/traffic_log.csv || echo 0" | grep -qv "^0$" 2>/dev/null
test_result "Traffic log CSV 有寫入資料" $?
echo ""
# ============================================================
# 7. WebSocket 測試
# ============================================================
echo "--- 7. WebSocket 測試 ---"
# 簡單測試 WS endpoint 是否回應 (upgrade request)
# curl -sv 輸出 status code 到 stderr101 Switching Protocols 表示成功
WS_CODE=$(ng_exec "timeout 3 curl -s -o /dev/null -w '%{http_code}' -H 'Upgrade: websocket' -H 'Connection: Upgrade' -H 'Sec-WebSocket-Key: dGVzdA==' -H 'Sec-WebSocket-Version: 13' $NG_API/ws/health 2>/dev/null || true")
echo "$WS_CODE" | grep -q "101"
test_result "WebSocket /ws/health 升級成功 (101)" $?
echo ""
# ============================================================
# 8. GeoIP 國家封鎖 API 測試
# ============================================================
echo "--- 8. GeoIP 國家封鎖 API 測試 ---"
# 8a. 查詢目前封鎖的國家列表
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/acl/geo/blocked" | grep -q "200"
test_result "GET /api/acl/geo/blocked" $?
# 8b. 封鎖國家 (CN, RU) — GeoIP rebuild 需要掃描 MaxMind DBtimeout 加長到 30s
ng_exec "timeout 30 curl -s -o /dev/null -w '%{http_code}' -X PUT '$NG_API/api/acl/geo/block' -H 'Content-Type: application/json' -d '{\"country_codes\":[\"CN\",\"RU\"]}'" | grep -q "200"
test_result "PUT /api/acl/geo/block 封鎖 CN, RU" $?
# 8c. 驗證封鎖列表包含 CN
GEO_BLOCKED=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/acl/geo/blocked")
echo "$GEO_BLOCKED" | grep -q "CN" 2>/dev/null
test_result "封鎖列表包含 CN" $?
# 8d. 回傳包含 total_prefixes
GEO_PUT_RESP=$(ng_exec "timeout 30 curl -s -X PUT '$NG_API/api/acl/geo/block' -H 'Content-Type: application/json' -d '{\"country_codes\":[\"KP\"]}'")
echo "$GEO_PUT_RESP" | grep -q "total_prefixes" 2>/dev/null
test_result "PUT 回傳 total_prefixes 欄位" $?
# 8e. 解除封鎖
ng_exec "timeout 30 curl -s -o /dev/null -w '%{http_code}' -X DELETE '$NG_API/api/acl/geo/unblock' -H 'Content-Type: application/json' -d '{\"country_codes\":[\"CN\",\"RU\",\"KP\"]}'" | grep -q "200"
test_result "DELETE /api/acl/geo/unblock 清除所有 GeoIP 規則" $?
# 8f. 驗證清空
GEO_AFTER=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/acl/geo/blocked")
echo "$GEO_AFTER" | grep -q '"blocked_countries":\[\]' 2>/dev/null || echo "$GEO_AFTER" | grep -q '"blocked_countries": \[\]' 2>/dev/null
test_result "封鎖列表已清空" $?
echo ""
# ============================================================
# 9. DNS 黑名單 API 測試
# ============================================================
echo "--- 9. DNS 黑名單 API 測試 ---"
# 9a. 查詢 DNS 黑名單
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/filter/dns/blacklist" | grep -q "200"
test_result "GET /api/filter/dns/blacklist" $?
# 9b. 新增域名到黑名單
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X PUT '$NG_API/api/filter/dns/blacklist' -H 'Content-Type: application/json' -d '{\"domains\":[\"malware.example.com\",\"phishing.test.org\"]}'" | grep -q "200"
test_result "PUT /api/filter/dns/blacklist 新增域名" $?
# 9c. 驗證黑名單已更新
DNS_DOMAINS=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/filter/dns/blacklist")
echo "$DNS_DOMAINS" | grep -q "malware.example.com" 2>/dev/null
test_result "黑名單包含 malware.example.com" $?
# 9d. 移除域名
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X DELETE '$NG_API/api/filter/dns/blacklist' -H 'Content-Type: application/json' -d '{\"domains\":[\"phishing.test.org\"]}'" | grep -q "200"
test_result "DELETE 移除 phishing.test.org" $?
# 9e. 驗證移除結果
DNS_AFTER=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/filter/dns/blacklist")
echo "$DNS_AFTER" | grep -q "malware.example.com" 2>/dev/null
test_result "移除後仍包含 malware.example.com" $?
# 9f. 清除所有 DNS 黑名單
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X DELETE '$NG_API/api/filter/dns/blacklist' -H 'Content-Type: application/json' -d '{\"domains\":[\"malware.example.com\"]}'" | grep -q "200"
test_result "清除所有 DNS 黑名單規則" $?
echo ""
# ============================================================
# 10. DNS 黑名單封鎖流量驗證
# ============================================================
echo "--- 10. DNS 黑名單封鎖流量驗證 ---"
# 10a. 新增測試域名到黑名單
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X PUT '$NG_API/api/filter/dns/blacklist' -H 'Content-Type: application/json' -d '{\"domains\":[\"evil.example.com\"]}'" | grep -q "200"
test_result "新增 evil.example.com 到 DNS 黑名單" $?
# 10b. 對黑名單域名的 DNS 查詢應被丟棄 (timeout)
sleep 1
ext_exec "timeout 3 dig @10.10.2.2 evil.example.com +time=2 +tries=1 >/dev/null 2>&1" && DNS_BLOCKED=1 || DNS_BLOCKED=0
test_result "evil.example.com DNS 查詢被封鎖" $DNS_BLOCKED
# 10c. 子域名也應被封鎖
ext_exec "timeout 3 dig @10.10.2.2 sub.evil.example.com +time=2 +tries=1 >/dev/null 2>&1" && SUB_BLOCKED=1 || SUB_BLOCKED=0
test_result "sub.evil.example.com 子域名也被封鎖" $SUB_BLOCKED
# 10d. 清除
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X DELETE '$NG_API/api/filter/dns/blacklist' -H 'Content-Type: application/json' -d '{\"domains\":[\"evil.example.com\"]}'" | grep -q "200"
test_result "清除 DNS 黑名單測試規則" $?
echo ""
# ============================================================
# 結果總結
# ============================================================
echo "=========================================="
echo " 測試結果: $PASS 通過 / $FAIL 失敗 / $((PASS + FAIL)) 總計"
echo "=========================================="
if [ "$FAIL" -gt 0 ]; then
echo ""
echo "失敗的測試:"
for t in "${TESTS[@]}"; do
name="${t%:*}"
result="${t##*:}"
if [ "$result" -ne 0 ]; then
echo "$name"
fi
done
fi
exit $FAIL