From 4d85caf96e5cd5a21719c458badfadb593e5284c Mon Sep 17 00:00:00 2001 From: Marko Kreen Date: Mon, 3 Aug 2015 21:58:23 +0300 Subject: [PATCH] Support pg_hba.conf-style syntax Also add peer auth. Main reason to have it is that unix and tcp connections may want different auth and configuring it in plain .ini is pain. As a bonus it provides ip-based filtering too. No username mapping yet though. --- Makefile | 1 + include/bouncer.h | 6 + include/hba.h | 24 ++ include/system.h | 2 + src/client.c | 24 +- src/hba.c | 735 ++++++++++++++++++++++++++++++++++++++++++++ src/main.c | 14 + src/system.c | 20 ++ test/Makefile | 34 +- test/hba_test.c | 122 ++++++++ test/hba_test.eval | 82 +++++ test/hba_test.rules | 51 +++ 12 files changed, 1100 insertions(+), 15 deletions(-) create mode 100644 include/hba.h create mode 100644 src/hba.c create mode 100644 test/hba_test.c create mode 100644 test/hba_test.eval create mode 100644 test/hba_test.rules diff --git a/Makefile b/Makefile index b9825b7..6058c98 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ pgbouncer_SOURCES = \ src/admin.c \ src/client.c \ src/dnslookup.c \ + src/hba.c \ src/janitor.c \ src/loader.c \ src/main.c \ diff --git a/include/bouncer.h b/include/bouncer.h index 084fce7..d409227 100644 --- a/include/bouncer.h +++ b/include/bouncer.h @@ -108,6 +108,7 @@ extern int cf_sbuf_len; #include "stats.h" #include "takeover.h" #include "janitor.h" +#include "hba.h" /* to avoid allocations will use static buffers */ #define MAX_DBNAME 64 @@ -122,6 +123,9 @@ extern int cf_sbuf_len; #define AUTH_MD5 5 #define AUTH_CREDS 6 #define AUTH_CERT 7 +#define AUTH_PEER 8 +#define AUTH_HBA 9 +#define AUTH_REJECT 10 /* type codes for weird pkts */ #define PKT_STARTUP_V2 0x20000 @@ -415,6 +419,7 @@ extern usec_t cf_dns_zone_check_period; extern int cf_auth_type; extern char *cf_auth_file; extern char *cf_auth_query; +extern char *cf_auth_hba_file; extern char *cf_pidfile; @@ -464,6 +469,7 @@ extern const struct CfLookup pool_mode_map[]; extern usec_t g_suspend_start; extern struct DNSContext *adns; +extern struct HBA *parsed_hba; static inline PgSocket * _MUSTCHECK pop_socket(struct StatList *slist) diff --git a/include/hba.h b/include/hba.h new file mode 100644 index 0000000..8e6f1eb --- /dev/null +++ b/include/hba.h @@ -0,0 +1,24 @@ +/* + * Host-Based-Access-control file support. + * + * Copyright (c) 2015 Marko Kreen + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +struct HBA; + +struct HBA *hba_load_rules(const char *fn); +void hba_free(struct HBA *hba); +int hba_eval(struct HBA *hba, PgAddr *addr, bool is_tls, const char *dbname, const char *username); + diff --git a/include/system.h b/include/system.h index cb531ae..ab8e0aa 100644 --- a/include/system.h +++ b/include/system.h @@ -67,6 +67,8 @@ static inline char *crypt(const char *p, const char *s) { return NULL; } static inline int lstat(const char *path, struct stat *st) { return stat(path, st); } #endif +bool check_unix_peer_name(int fd, const char *username); + void change_user(const char *user); void change_file_mode(const char *fn, mode_t mode, const char *user, const char *group); diff --git a/src/client.c b/src/client.c index 70205b8..1bc549a 100644 --- a/src/client.c +++ b/src/client.c @@ -171,10 +171,23 @@ fail: return false; } +static bool login_as_unix_peer(PgSocket *client) +{ + if (!pga_is_unix(&client->remote_addr)) + goto fail; + if (!check_unix_peer_name(sbuf_socket(&client->sbuf), client->auth_user->name)) + goto fail; + return finish_client_login(client); +fail: + disconnect_client(client, true, "unix socket login rejected"); + return false; +} + static bool finish_set_pool(PgSocket *client, bool takeover) { PgUser *user = client->auth_user; bool ok = false; + int auth; /* pool user may be forced */ if (client->db->forced_user) { @@ -212,7 +225,13 @@ static bool finish_set_pool(PgSocket *client, bool takeover) if (client->own_user) return finish_client_login(client); - switch (cf_auth_type) { + auth = cf_auth_type; + if (auth == AUTH_HBA) { + auth = hba_eval(parsed_hba, &client->remote_addr, !!client->sbuf.tls, + client->db->name, client->auth_user->name); + } + + switch (auth) { case AUTH_ANY: case AUTH_TRUST: ok = finish_client_login(client); @@ -225,6 +244,9 @@ static bool finish_set_pool(PgSocket *client, bool takeover) case AUTH_CERT: ok = login_via_cert(client); break; + case AUTH_PEER: + ok = login_as_unix_peer(client); + break; default: disconnect_client(client, true, "login rejected"); ok = false; diff --git a/src/hba.c b/src/hba.c new file mode 100644 index 0000000..7402bc6 --- /dev/null +++ b/src/hba.c @@ -0,0 +1,735 @@ +/* + * Host-Based-Access-control file support. + * + * Copyright (c) 2015 Marko Kreen + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "bouncer.h" + +#include +#include + +enum RuleType { + RULE_LOCAL, + RULE_HOST, + RULE_HOSTSSL, + RULE_HOSTNOSSL, +}; + +#define NAME_ALL 1 +#define NAME_SAMEUSER 2 + +struct NameSlot { + size_t strlen; + char str[]; +}; + +struct HBAName { + unsigned int flags; + struct StrSet *name_set; +}; + +struct HBARule { + struct List node; + enum RuleType rule_type; + int rule_method; + int rule_af; + uint8_t rule_addr[16]; + uint8_t rule_mask[16]; + struct HBAName db_name; + struct HBAName user_name; +}; + +struct HBA { + struct List rules; +}; + +/* + * StrSet + */ + +struct StrSetNode { + unsigned int s_len; + char s_val[FLEX_ARRAY]; +}; + +struct StrSet { + CxMem *pool; + unsigned count; + unsigned alloc; + struct StrSetNode **nodes; + struct CBTree *cbtree; +}; + +struct StrSet *strset_new(CxMem *cx); +void strset_free(struct StrSet *set); +bool strset_add(struct StrSet *set, const char *str, unsigned int len); +bool strset_contains(struct StrSet *set, const char *str, unsigned int len); + +struct StrSet *strset_new(CxMem *cx) +{ + struct StrSet *set; + CxMem *pool; + + pool = cx_new_pool(cx, 1024, 0); + if (!pool) + return NULL; + set = cx_alloc(pool, sizeof *set); + if (!set) + return NULL; + set->pool = pool; + set->cbtree = NULL; + set->count = 0; + set->alloc = 10; + set->nodes = cx_alloc0(pool, set->alloc * sizeof(struct StrSet *)); + if (!set->nodes) { + cx_destroy(pool); + return NULL; + } + return set; +} + +static size_t strset_node_key(void *ctx, void *obj, const void **ptr_p) +{ + struct StrSetNode *node = obj; + *ptr_p = node->s_val; + return node->s_len; +} + +bool strset_add(struct StrSet *set, const char *str, unsigned int len) +{ + struct StrSetNode *node; + unsigned int i; + bool ok; + + if (strset_contains(set, str, len)) + return true; + + node = cx_alloc(set->pool, offsetof(struct StrSetNode, s_val) + len + 1); + if (!node) + return false; + node->s_len = len; + memcpy(node->s_val, str, len); + node->s_val[len] = 0; + + if (set->count < set->alloc) { + set->nodes[set->count++] = node; + return true; + } + + if (!set->cbtree) { + set->cbtree = cbtree_create(strset_node_key, NULL, set, set->pool); + if (!set->cbtree) + return false; + for (i = 0; i < set->count; i++) { + ok = cbtree_insert(set->cbtree, set->nodes[i]); + if (!ok) + return false; + } + } + ok = cbtree_insert(set->cbtree, node); + if (!ok) + return false; + set->count++; + return true; +} + +bool strset_contains(struct StrSet *set, const char *str, unsigned int len) +{ + unsigned int i; + struct StrSetNode *node; + if (set->cbtree) + return cbtree_lookup(set->cbtree, str, len) != NULL; + for (i = 0; i < set->count; i++) { + node = set->nodes[i]; + if (node->s_len != len) + continue; + if (memcmp(node->s_val, str, len) == 0) + return true; + } + return false; +} + +void strset_free(struct StrSet *set) +{ + if (set) + cx_destroy(set->pool); +} + +/* + * Parse HBA tokens. + */ + +enum TokType { + TOK_STRING, + TOK_IDENT, + TOK_COMMA, + TOK_FAIL, + TOK_EOL +}; + +struct TokParser { + const char *pos; + enum TokType cur_tok; + char *cur_tok_str; + + char *buf; + size_t buflen; +}; + +static bool tok_buf_check(struct TokParser *p, size_t len) +{ + size_t tmplen; + char *tmp; + if (p->buflen >= len) + return true; + tmplen = len*2; + tmp = realloc(p->buf, tmplen); + if (!tmp) + return false; + p->buf = tmp; + p->buflen = tmplen; + return true; +} + +static enum TokType next_token(struct TokParser *p) +{ + const char *s, *s2; + char *dst; + if (p->cur_tok == TOK_EOL) + return TOK_EOL; + p->cur_tok_str = NULL; + p->cur_tok = TOK_FAIL; + + while (p->pos[0] && isspace((unsigned char)p->pos[0])) + p->pos++; + + if (p->pos[0] == '#' || p->pos[0] == '\0') { + p->cur_tok = TOK_EOL; + p->pos = NULL; + } else if (p->pos[0] == ',') { + p->cur_tok = TOK_COMMA; + p->pos++; + } else if (p->pos[0] == '"') { + for (s = p->pos+1; s[0]; s++) { + if (s[0] == '"') { + if (s[1] == '"') + s++; + else + break; + } + } + if (s[0] != '"' || !tok_buf_check(p, s - p->pos)) + return TOK_FAIL; + dst = p->buf; + for (s2 = p->pos+1; s2 < s; s2++) { + *dst++ = *s2; + if (*s2 == '"') s2++; + } + *dst = 0; + p->pos = s + 1; + p->cur_tok = TOK_STRING; + p->cur_tok_str = p->buf; + } else { + for (s = p->pos + 1; *s; s++) { + if (*s == ',' || *s == '#' || *s == '"') + break; + if (isspace((unsigned char)*s)) + break; + } + if (!tok_buf_check(p, s - p->pos)) + return TOK_FAIL; + memcpy(p->buf, p->pos, s - p->pos); + p->buf[s - p->pos] = 0; + p->pos = s; + p->cur_tok = TOK_IDENT; + p->cur_tok_str = p->buf; + + } + return p->cur_tok; +} + +static bool eat(struct TokParser *p, enum TokType ttype) +{ + if (p->cur_tok == ttype) { + next_token(p); + return true; + } + return false; +} + +static bool eat_kw(struct TokParser *p, const char *kw) +{ + if (p->cur_tok == TOK_IDENT && strcmp(kw, p->cur_tok_str) == 0) { + next_token(p); + return true; + } + return false; +} + +static bool expect(struct TokParser *tp, enum TokType ttype, const char **str_p) +{ + if (tp->cur_tok == ttype) { + *str_p = tp->buf; + return true; + } + return false; +} + +static char *path_join(const char *p1, const char *p2) +{ + size_t len1, len2; + char *res = NULL, *pos; + + if (p2[0] == '/' || p1[0] == 0 || !memcmp(p1, ".", 2)) + return strdup(p2); + len1 = strlen(p1); + len2 = strlen(p2); + res = malloc(len1 + len2 + 2 + 1); + if (res) { + memcpy(res, p1, len1); + pos = res + len1; + if (pos[-1] != '/') + *pos++ = '/'; + memcpy(pos, p2, len2 + 1); + } + return res; +} + +static char *path_join_dirname(const char *parent, const char *fn) +{ + char *tmp, *res; + const char *basedir; + if (fn[0] == '/') + return strdup(fn); + tmp = strdup(parent); + if (!tmp) + return NULL; + basedir = dirname(tmp); + res = path_join(basedir, fn); + free(tmp); + return res; +} + +static void init_parser(struct TokParser *p) +{ + memset(p, 0, sizeof(*p)); +} + +static void parse_from_string(struct TokParser *p, const char *str) +{ + p->pos = str; + p->cur_tok = TOK_COMMA; + p->cur_tok_str = NULL; + next_token(p); +} + +static void free_parser(struct TokParser *p) +{ + free(p->buf); + p->buf = NULL; +} + +static bool parse_names(struct HBAName *hname, struct TokParser *p, bool is_db, const char *parent_filename); + +static bool parse_namefile(struct HBAName *hname, const char *fn, bool is_db) +{ + FILE *f; + ssize_t len; + char *ln = NULL; + size_t buflen = 0; + int linenr; + bool ok = false; + struct TokParser tp; + + init_parser(&tp); + + f = fopen(fn, "r"); + if (!f) { + free(fn); + return false; + } + for (linenr = 1; ; linenr++) { + len = getline(&ln, &buflen, f); + if (len < 0) { + ok = true; + break; + } + parse_from_string(&tp, ln); + if (!parse_names(hname, &tp, is_db, fn)) + break; + } + free_parser(&tp); + free(fn); + free(ln); + fclose(f); + return ok; +} + +static bool parse_names(struct HBAName *hname, struct TokParser *tp, bool is_db, const char *parent_filename) +{ + const char *tok; + while (1) { + if (eat_kw(tp, "all")) { + hname->flags |= NAME_ALL; + goto eat_comma; + } + if (is_db) { + if (eat_kw(tp, "sameuser")) { + hname->flags |= NAME_SAMEUSER; + goto eat_comma; + } + if (eat_kw(tp, "samerole")) { + return false; + } + if (eat_kw(tp, "samegroup")) { + return false; + } + if (eat_kw(tp, "replication")) { + return false; + } + } + + if (expect(tp, TOK_IDENT, &tok)) { + if (tok[0] == '+') { + return false; + } + + if (tok[0] == '@') { + bool ok; + const char *fn; + fn = path_join_dirname(parent_filename, tok + 1); + if (!fn) + return false; + ok = parse_namefile(hname, fn, is_db); + free(fn); + if (!ok) + return false; + goto eat_comma; + } + /* fallthrough */ + } else if (expect(tp, TOK_STRING, &tok)) { + /* fallthrough */ + } else { + return false; + } + + /* + * TOK_IDENT or TOK_STRING as plain name. + */ + + if (!hname->name_set) { + hname->name_set = strset_new(NULL); + if (!hname->name_set) + return false; + } + if (!strset_add(hname->name_set, tok, strlen(tok))) + return false; + next_token(tp); +eat_comma: + if (!eat(tp, TOK_COMMA)) + break; + } + return true; +} + +static void rule_free(struct HBARule *rule) +{ + free(rule); +} + +static bool parse_addr(struct HBARule *rule, const char *addr) +{ + if (inet_pton(AF_INET6, addr, rule->rule_addr)) { + rule->rule_af = AF_INET6; + } else if (inet_pton(AF_INET, addr, rule->rule_addr)) { + rule->rule_af = AF_INET; + } else { + return false; + } + return true; +} + +static bool parse_nmask(struct HBARule *rule, const char *nmask) +{ + char *end = NULL; + unsigned long bits; + unsigned int i; + errno = 0; + bits = strtoul(nmask, &end, 10); + if (errno || *end) { + return false; + } + if (rule->rule_af == AF_INET && bits > 32) { + return false; + } + if (rule->rule_af == AF_INET6 && bits > 128) { + return false; + } + for (i = 0; i < bits/8; i++) + rule->rule_mask[i] = 255; + if (bits % 8) + rule->rule_mask[i] = 255 << (8 - (bits % 8)); + return true; +} + +static bool bad_mask(struct HBARule *rule) +{ + int i, bytes = rule->rule_af == AF_INET ? 4 : 16; + uint8_t res = 0; + for (i = 0; i < bytes; i++) + res |= rule->rule_addr[i] & (255 ^ rule->rule_mask[i]); + return !!res; +} + +static bool parse_line(struct HBA *hba, struct TokParser *tp, int linenr, const char *parent_filename) +{ + const char *addr = NULL, *mask = NULL; + enum RuleType rtype; + char *nmask = NULL; + struct HBARule *rule = NULL; + + if (eat_kw(tp, "local")) { + rtype = RULE_LOCAL; + } else if (eat_kw(tp, "host")) { + rtype = RULE_HOST; + } else if (eat_kw(tp, "hostssl")) { + rtype = RULE_HOSTSSL; + } else if (eat_kw(tp, "hostnossl")) { + rtype = RULE_HOSTNOSSL; + } else if (eat(tp, TOK_EOL)) { + return true; + } else { + log_warning("hba line %d: unknown type", linenr); + return false; + } + + rule = calloc(sizeof *rule, 1); + if (!rule) { + log_warning("hba: no mem for rule"); + goto failed; + } + rule->rule_type = rtype; + + if (!parse_names(&rule->db_name, tp, true, parent_filename)) + goto failed; + if (!parse_names(&rule->user_name, tp, true, parent_filename)) + goto failed; + + if (rtype == RULE_LOCAL) { + rule->rule_af = AF_UNIX; + } else { + if (!expect(tp, TOK_IDENT, &addr)) { + log_warning("hba line %d: did not find address - %d - '%s'", linenr, tp->cur_tok, tp->buf); + goto failed; + } + nmask = strchr(addr, '/'); + if (nmask) { + *nmask++ = 0; + } + + if (!parse_addr(rule, addr)) { + log_warning("hba line %d: failed to parse address - %s", linenr, addr); + goto failed; + } + + if (nmask) { + if (!parse_nmask(rule, nmask)) { + log_warning("hba line %d: invalid mask", linenr); + goto failed; + } + next_token(tp); + } else { + next_token(tp); + if (!expect(tp, TOK_IDENT, &mask)) { + log_warning("hba line %d: did not find mask", linenr); + goto failed; + } + if (!inet_pton(rule->rule_af, mask, rule->rule_mask)) { + log_warning("hba line %d: failed to parse mask: %s", linenr, mask); + goto failed; + } + next_token(tp); + } + if (bad_mask(rule)) { + char buf1[128], buf2[128]; + log_warning("Addres does not match mask in %s line #%d: %s / %s", parent_filename, linenr, + inet_ntop(rule->rule_af, rule->rule_addr, buf1, sizeof buf1), + inet_ntop(rule->rule_af, rule->rule_mask, buf2, sizeof buf2)); + } + } + + if (eat_kw(tp, "trust")) { + rule->rule_method = AUTH_TRUST; + } else if (eat_kw(tp, "reject")) { + rule->rule_method = AUTH_REJECT; + } else if (eat_kw(tp, "md5")) { + rule->rule_method = AUTH_MD5; + } else if (eat_kw(tp, "password")) { + rule->rule_method = AUTH_PLAIN; + } else if (eat_kw(tp, "peer")) { + rule->rule_method = AUTH_PEER; + } else if (eat_kw(tp, "cert")) { + rule->rule_method = AUTH_CERT; + } else { + log_warning("hba line %d: unsupported method: buf=%s", linenr, tp->buf); + goto failed; + } + + if (!eat(tp, TOK_EOL)) { + log_warning("hba line %d: unsupported parameters", linenr); + goto failed; + } + + list_append(&hba->rules, &rule->node); + return true; +failed: + rule_free(rule); + return false; +} + +struct HBA *hba_load_rules(const char *fn) +{ + struct HBA *hba = NULL; + FILE *f = NULL; + char *ln = NULL; + size_t lnbuf = 0; + ssize_t len; + int linenr; + struct TokParser tp; + + init_parser(&tp); + + hba = malloc(sizeof *hba); + if (!hba) + goto out; + + f = fopen(fn, "r"); + if (!f) + goto out; + + list_init(&hba->rules); + for (linenr = 1; ; linenr++) { + len = getline(&ln, &lnbuf, f); + if (len < 0) + break; + parse_from_string(&tp, ln); + if (!parse_line(hba, &tp, linenr, fn)) { + free(hba); + hba = NULL; + break; + } + } +out: + free_parser(&tp); + free(ln); + if (f) + fclose(f); + return hba; +} + +void hba_free(struct HBA *hba) +{ + struct List *el, *tmp; + struct HBARule *rule; + if (!hba) + return; + list_for_each_safe(el, &hba->rules, tmp) { + rule = container_of(el, struct HBARule, node); + list_del(&rule->node); + rule_free(rule); + } + free(hba); +} + +static bool name_match(struct HBAName *hname, const char *name, unsigned int namelen, const char *pair) +{ + if (hname->flags & NAME_ALL) + return true; + if ((hname->flags & NAME_SAMEUSER) && strcmp(name, pair) == 0) + return true; + if (hname->name_set) + return strset_contains(hname->name_set, name, namelen); + return false; +} + +static bool match_inet4(const struct HBARule *rule, PgAddr *addr) +{ + const uint32_t *src, *base, *mask; + if (pga_family(addr) != AF_INET) + return false; + src = &addr->sin.sin_addr.s_addr; + base = (uint32_t *)rule->rule_addr; + mask = (uint32_t *)rule->rule_mask; + return (src[0] & mask[0]) == base[0]; +} + +static bool match_inet6(const struct HBARule *rule, PgAddr *addr) +{ + const uint32_t *src, *base, *mask; + if (pga_family(addr) != AF_INET6) + return false; + src = (uint32_t *)addr->sin6.sin6_addr.s6_addr; + base = (uint32_t *)rule->rule_addr; + mask = (uint32_t *)rule->rule_mask; + return (src[0] & mask[0]) == base[0] && (src[1] & mask[1]) == base[1] && + (src[2] & mask[2]) == base[2] && (src[3] & mask[3]) == base[3]; +} + +int hba_eval(struct HBA *hba, PgAddr *addr, bool is_tls, const char *dbname, const char *username) +{ + struct List *el; + struct HBARule *rule; + unsigned int dbnamelen = strlen(dbname); + unsigned int unamelen = strlen(username); + + if (!hba) + return AUTH_REJECT; + + list_for_each(el, &hba->rules) { + rule = container_of(el, struct HBARule, node); + + /* match address */ + if (pga_is_unix(addr)) { + if (rule->rule_type != RULE_LOCAL) + continue; + } else if (rule->rule_type == RULE_LOCAL) { + continue; + } else if (rule->rule_type == RULE_HOSTSSL && !is_tls) { + continue; + } else if (rule->rule_type == RULE_HOSTNOSSL && is_tls) { + continue; + } else if (rule->rule_af == AF_INET) { + if (!match_inet4(rule, addr)) + continue; + } else if (rule->rule_af == AF_INET6) { + if (!match_inet6(rule, addr)) + continue; + } else { + continue; + } + + /* match db & user */ + if (!name_match(&rule->db_name, dbname, dbnamelen, username)) + continue; + if (!name_match(&rule->user_name, username, unamelen, dbname)) + continue; + + /* rule matches */ + return rule->rule_method; + } + return AUTH_REJECT; +} + diff --git a/src/main.c b/src/main.c index a083e29..9495573 100644 --- a/src/main.c +++ b/src/main.c @@ -55,6 +55,8 @@ static void usage(int err, char *exe) /* async dns handler */ struct DNSContext *adns; +struct HBA *parsed_hba; + /* * configuration storage */ @@ -91,6 +93,7 @@ int cf_tcp_keepintvl; int cf_auth_type = AUTH_MD5; char *cf_auth_file; +char *cf_auth_hba_file; char *cf_auth_query; int cf_max_client_conn; @@ -174,6 +177,7 @@ static const struct CfLookup auth_type_map[] = { #endif { "md5", AUTH_MD5 }, { "cert", AUTH_CERT }, + { "hba", AUTH_HBA }, { NULL } }; @@ -212,6 +216,7 @@ CF_ABS("unix_socket_group", CF_STR, cf_unix_socket_group, CF_NO_RELOAD, ""), #endif CF_ABS("auth_type", CF_LOOKUP(auth_type_map), cf_auth_type, 0, "md5"), CF_ABS("auth_file", CF_STR, cf_auth_file, 0, "unconfigured_file"), +CF_ABS("auth_hba_file", CF_STR, cf_auth_hba_file, 0, ""), CF_ABS("auth_query", CF_STR, cf_auth_query, 0, "SELECT usename, passwd FROM pg_shadow WHERE usename=$1"), CF_ABS("pool_mode", CF_LOOKUP(pool_mode_map), cf_pool_mode, 0, "session"), CF_ABS("max_client_conn", CF_INT, cf_max_client_conn, 0, "100"), @@ -374,6 +379,15 @@ void load_config(void) set_dbs_dead(false); } + if (cf_auth_type == AUTH_HBA) { + struct HBA *hba = hba_load_rules(cf_auth_hba_file); + if (hba) { + if (parsed_hba) + hba_free(parsed_hba); + parsed_hba = hba; + } + } + /* reset pool_size, kill dbs */ config_postprocess(); diff --git a/src/system.c b/src/system.c index c7ed15c..c9d69c4 100644 --- a/src/system.c +++ b/src/system.c @@ -122,3 +122,23 @@ void change_file_mode(const char *fn, mode_t mode, } } +/* + * UNIX socket helper. + */ + +bool check_unix_peer_name(int fd, const char *username) +{ + int res; + uid_t peer_uid = -1; + gid_t peer_gid = -1; + struct passwd *pw; + + res = getpeereid(fd, &peer_uid, &peer_gid); + if (res < 0) + return false; + pw = getpwuid(peer_uid); + if (!pw) + return false; + return strcmp(pw->pw_name, username) == 0; +} + diff --git a/test/Makefile b/test/Makefile index c9bfbc7..368b598 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,22 +1,28 @@ -PGINC = -I$(shell pg_config --includedir) -PGLIB = -L$(shell pg_config --libdir) +#PGINC = -I$(shell pg_config --includedir) +#PGLIB = -L$(shell pg_config --libdir) +#CPPFLAGS += -I../include -I../lib $(PGINC) +#LDFLAGS += $(PGLIB) +#LIBS := -lpq $(LIBS) +#ifeq ($(PORTNAME),win32) +#CPPFLAGS += -I../win32 +#endif -include ../config.mak +USUAL_DIR = ../lib -CPPFLAGS += -I../include $(PGINC) -LDFLAGS += $(PGLIB) -LIBS := -lpq $(LIBS) +noinst_PROGRAMS = hba_test +hba_test_CPPFLAGS = -I../include +hba_test_CFLAGS = -O0 +hba_test_SOURCES = hba_test.c ../src/hba.c ../src/util.c +hba_test_EMBED_LIBUSUAL = 1 -ifeq ($(PORTNAME),win32) -CPPFLAGS += -I../win32 -endif +AM_FEATURES = libusual -all: asynctest +include ../config.mak +include ../lib/mk/antimake.mk -asynctest: asynctest.c - $(CC) -o $@ $< $(DEFS) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $(LIBS) +all: run_test -clean: - rm -f asynctest +run_test: hba_test + ./hba_test diff --git a/test/hba_test.c b/test/hba_test.c new file mode 100644 index 0000000..776c951 --- /dev/null +++ b/test/hba_test.c @@ -0,0 +1,122 @@ + +#include "bouncer.h" + +#include +#include +#include +#include +#include +#include +#include + +int cf_tcp_keepcnt; +int cf_tcp_keepintvl; +int cf_tcp_keepidle; +int cf_tcp_keepalive; +int cf_tcp_socket_buffer; +int cf_listen_port; + +static const char *method2string[] = { + "trust", + "x1", + "x2", + "password", + "crypt", + "md5", + "creds", + "cert", + "peer", + "hba", + "reject", +}; + +static char *get_token(char **ln_p) +{ + char *ln = *ln_p, *tok, *end; + + while (*ln && *ln == '\t') ln++; + tok = ln; + while (*ln && *ln != '\t') ln++; + end = ln; + while (*ln && *ln == '\t') ln++; + + *ln_p = ln; + if (tok == end) + return NULL; + *end = 0; + return tok; +} + +static int hba_test_eval(struct HBA *hba, char *ln, int linenr) +{ + const char *addr=NULL, *user=NULL, *db=NULL, *tls=NULL, *exp=NULL; + PgAddr pgaddr; + int res; + + if (ln[0] == '#') + return 0; + exp = get_token(&ln); + db = get_token(&ln); + user = get_token(&ln); + addr = get_token(&ln); + tls = get_token(&ln); + if (!exp) + return 0; + if (!db || !user) + die("hbatest: invalid line #%d", linenr); + + if (!pga_pton(&pgaddr, addr, 9999)) + die("hbatest: invalid addr on line #%d", linenr); + + res = hba_eval(hba, &pgaddr, !!tls, db, user); + if (strcmp(method2string[res], exp) == 0) { + res = 0; + } else { + log_warning("FAIL on line %d: expected '%s' got '%s' - user=%s db=%s addr=%s", + linenr, exp, method2string[res], user, db, addr); + res = 1; + } + return res; +} + +static void hba_test(void) +{ + struct HBA *hba; + FILE *f; + char *ln = NULL; + size_t lnbuf = 0; + ssize_t len; + int linenr; + int nfailed = 0; + + hba = hba_load_rules("hba_test.rules"); + if (!hba) + die("hbatest: did not find config"); + + f = fopen("hba_test.eval", "r"); + if (!f) + die("hbatest: cannot open eval"); + + for (linenr = 1; ; linenr++) { + len = getline(&ln, &lnbuf, f); + if (len < 0) + break; + if (len && ln[len-1] == '\n') + ln[len-1] = 0; + nfailed += hba_test_eval(hba, ln, linenr); + } + free(ln); + fclose(f); + hba_free(hba); + if (nfailed) + errx(1, "HBA test failures: %d", nfailed); + else + printf("HBA test OK\n"); +} + +int main(void) +{ + hba_test(); + return 0; +} + diff --git a/test/hba_test.eval b/test/hba_test.eval new file mode 100644 index 0000000..eecb4fa --- /dev/null +++ b/test/hba_test.eval @@ -0,0 +1,82 @@ + +# peer +md5 db user unix +peer dbp user unix +password db userp unix +trust dbz userz unix + +# hostssl +cert db user 10.1.1.1 tls +reject db user 10.1.1.1 +reject db user 13.1.1.1 + +# hostnossl +reject db user 11.1.1.1 tls +md5 db user 11.1.1.1 +reject db user 13.1.1.1 + +# host +password db user 127.0.0.2 tls +password db user 127.0.0.3 +reject db user 127.0.1.4 + +# db1 filt +reject db1x user 127.0.1.4 +md5 db1 user 127.0.1.4 + +# user1 filt +md5 db1z user1 15.0.0.1 +reject db1z user2 15.0.0.1 + +# someusers +reject db2 user 16.0.0.1 +md5 db2 user1 16.0.0.1 +md5 db2 user2 16.0.0.1 +md5 db2 user3 16.0.0.1 +reject db2 user4 16.0.0.1 + +# manyusers +md5 db2 u1 17.0.0.1 +md5 db2 u2 17.0.0.1 +md5 db2 u3 17.0.0.1 +md5 db2 u4 17.0.0.1 +md5 db2 u5 17.0.0.1 +md5 db2 u6 17.0.0.1 +md5 db2 u7 17.0.0.1 +md5 db2 u8 17.0.0.1 +md5 db2 u9 17.0.0.1 +md5 db2 u10 17.0.0.1 +md5 db2 u11 17.0.0.1 + +# manydbs +reject d1 user 18.0.0.2 +trust d1 t18user 18.0.0.2 +trust d2 t18user 18.0.0.2 +trust d3 t18user 18.0.0.2 +trust d4 t18user 18.0.0.2 +trust d5 t18user 18.0.0.2 +trust d6 t18user 18.0.0.2 +trust d7 t18user 18.0.0.2 +trust d8 t18user 18.0.0.2 +trust d9 t18user 18.0.0.2 +trust d10 t18user 18.0.0.2 +trust d11 t18user 18.0.0.2 + +# quoting +reject db t19user 19.0.0.2 +cert all all 19.0.0.2 +cert q1"q2 a , b 19.0.0.2 + +# bitmask +cert mdb muser 199.199.199.199 +reject mdb muser 199.199.199.198 +reject mdb muser 199.199.199.200 + +cert mdb2 muser 254.1.1.1 + +# ipv6 +md5 mdb muser ff11:2::1 +md5 mdb muser ff22:3::1 +trust mdb muser ::1 +reject mdb muser ::2 + diff --git a/test/hba_test.rules b/test/hba_test.rules new file mode 100644 index 0000000..51d3459 --- /dev/null +++ b/test/hba_test.rules @@ -0,0 +1,51 @@ + +# METHOD: trust, reject, md5, password, peer, cert + +# local DATABASE USER METHOD [OPTIONS] +# host DATABASE USER ADDRESS METHOD [OPTIONS] +# hostssl DATABASE USER ADDRESS METHOD [OPTIONS] +# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] + +# ws + + # z + +# testing + +local dbp all peer +local all userp password +local dbz userz trust +local all all md5 + +hostssl all all 10.0.0.0/8 cert +hostnossl all all 11.0.0.0/8 md5 +host all all 127.0.0.0 255.255.255.0 password + +host db1 all 0.0.0.0/0 md5 +host all user1 15.0.0.0/8 md5 + +host tmp1,all user1,user2 , user3 16.0.0.0/8 md5 +host tmp2,all u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11,u2 17.0.0.0/8 md5 +host d1,d2,d3,d4,d5,d6,d7,d8,d9,d10,d11 t18user 18.0.0.0/8 trust # comment + +host "all" "all" 19.0.0.0/8 cert +host "q1""q2" "a , b" 19.0.0.0/8 cert + +# mask +host mdb muser 199.199.199.199/32 cert + +host mdb2 muser 128.0.0.0/9 trust +host mdb2 muser 128.0.0.0/8 md5 +host mdb2 muser 128.0.0.0/7 cert +host mdb2 muser 128.0.0.0/6 password +host mdb2 muser 128.0.0.0/5 cert +host mdb2 muser 128.0.0.0/4 trust +host mdb2 muser 128.0.0.0/3 md5 +host mdb2 muser 128.0.0.0/2 password +host mdb2 muser 128.0.0.0/1 cert + +# ipv6 +host mdb muser ff11::0/16 md5 +host mdb muser ff20::/12 md5 +host mdb muser ::1/128 trust + -- 2.39.5