提交 0f01192e 编写于 作者: D Daniel P. Berrange 提交者: Eric Blake

Add support for sVirt in the LXC driver

For the sake of backwards compat, LXC guests are *not*
confined by default. This is because it is not practical
to dynamically relabel containers using large filesystem
trees. Applications can create confined containers though,
by giving suitable XML configs

* src/Makefile.am: Link libvirt_lxc to security drivers
* src/lxc/libvirtd_lxc.aug, src/lxc/lxc_conf.h,
  src/lxc/lxc_conf.c, src/lxc/lxc.conf,
  src/lxc/test_libvirtd_lxc.aug: Config file handling for
  security driver
* src/lxc/lxc_driver.c: Wire up security driver functions
* src/lxc/lxc_controller.c: Add a '--security' flag to
  specify which security driver to activate
* src/lxc/lxc_container.c, src/lxc/lxc_container.h: Set
  the process label just before exec'ing init.
上级 b170eb99
......@@ -1506,7 +1506,14 @@ libvirt_lxc_SOURCES = \
$(DOMAIN_CONF_SOURCES) \
$(SECRET_CONF_SOURCES) \
$(CPU_CONF_SOURCES) \
$(SECURITY_DRIVER_SOURCES) \
$(NWFILTER_PARAM_CONF_SOURCES)
if WITH_SECDRIVER_SELINUX
libvirt_lxc_SOURCES += $(SECURITY_DRIVER_SELINUX_SOURCES)
endif
if WITH_SECDRIVER_APPARMOR
libvirt_lxc_SOURCES += $(SECURITY_DRIVER_APPARMOR_SOURCES)
endif
libvirt_lxc_LDFLAGS = $(WARN_CFLAGS) $(AM_LDFLAGS)
libvirt_lxc_LDADD = $(CAPNG_LIBS) $(YAJL_LIBS) \
$(LIBXML_LIBS) $(NUMACTL_LIBS) $(THREAD_LIBS) \
......@@ -1516,6 +1523,9 @@ libvirt_lxc_LDADD = $(CAPNG_LIBS) $(YAJL_LIBS) \
if WITH_DTRACE
libvirt_lxc_LDADD += probes.o
endif
if WITH_SECDRIVER_SELINUX
libvirt_lxc_LDADD += $(SELINUX_LIBS)
endif
libvirt_lxc_CFLAGS = \
$(LIBPARTED_CFLAGS) \
$(NUMACTL_CFLAGS) \
......@@ -1528,6 +1538,9 @@ if HAVE_LIBBLKID
libvirt_lxc_CFLAGS += $(BLKID_CFLAGS)
libvirt_lxc_LDADD += $(BLKID_LIBS)
endif
if WITH_SECDRIVER_SELINUX
libvirt_lxc_CFLAGS += $(SELINUX_CFLAGS)
endif
endif
endif
EXTRA_DIST += $(LXC_CONTROLLER_SOURCES)
......
......@@ -7,13 +7,26 @@ module Libvirtd_lxc =
let value_sep = del /[ \t]*=[ \t]*/ " = "
let indent = del /[ \t]*/ ""
let array_sep = del /,[ \t\n]*/ ", "
let array_start = del /\[[ \t\n]*/ "[ "
let array_end = del /\]/ "]"
let str_val = del /\"/ "\"" . store /[^\"]*/ . del /\"/ "\""
let bool_val = store /0|1/
let int_val = store /[0-9]+/
let str_array_element = [ seq "el" . str_val ] . del /[ \t\n]*/ ""
let str_array_val = counter "el" . array_start . ( str_array_element . ( array_sep . str_array_element ) * ) ? . array_end
let str_entry (kw:string) = [ key kw . value_sep . str_val ]
let bool_entry (kw:string) = [ key kw . value_sep . bool_val ]
let int_entry (kw:string) = [ key kw . value_sep . int_val ]
let str_array_entry (kw:string) = [ key kw . value_sep . str_array_val ]
(* Config entry grouped by function - same order as example config *)
let log_entry = bool_entry "log_with_libvirtd"
| str_entry "security_driver"
| bool_entry "security_default_confined"
| bool_entry "security_require_confined"
(* Each enty in the config is one of the following three ... *)
let entry = log_entry
......
......@@ -11,3 +11,21 @@
# This is disabled by default, uncomment below to enable it.
#
# log_with_libvirtd = 1
# The default security driver is SELinux. If SELinux is disabled
# on the host, then the security driver will automatically disable
# itself. If you wish to disable QEMU SELinux security driver while
# leaving SELinux enabled for the host in general, then set this
# to 'none' instead.
#
# security_driver = "selinux"
# If set to non-zero, then the default security labeling
# will make guests confined. If set to zero, then guests
# will be unconfined by default. Defaults to 0.
# security_default_confined = 1
# If set to non-zero, then attempts to create unconfined
# guests will be blocked. Defaults to 0.
# security_require_confined = 1
......@@ -49,7 +49,7 @@ static int lxcDefaultConsoleType(const char *ostype ATTRIBUTE_UNUSED)
/* Functions */
virCapsPtr lxcCapsInit(void)
virCapsPtr lxcCapsInit(lxc_driver_t *driver)
{
struct utsname utsname;
virCapsPtr caps;
......@@ -127,8 +127,30 @@ virCapsPtr lxcCapsInit(void)
/* LXC Requires an emulator in the XML */
virCapabilitiesSetEmulatorRequired(caps);
if (driver) {
/* Security driver data */
const char *doi, *model;
doi = virSecurityManagerGetDOI(driver->securityManager);
model = virSecurityManagerGetModel(driver->securityManager);
if (STRNEQ(model, "none")) {
if (!(caps->host.secModel.model = strdup(model)))
goto no_memory;
if (!(caps->host.secModel.doi = strdup(doi)))
goto no_memory;
}
VIR_DEBUG("Initialized caps for security driver \"%s\" with "
"DOI \"%s\"", model, doi);
} else {
VIR_INFO("No driver, not initializing security driver");
}
return caps;
no_memory:
virReportOOMError();
error:
virCapabilitiesFree(caps);
return NULL;
......@@ -140,6 +162,9 @@ int lxcLoadDriverConfig(lxc_driver_t *driver)
virConfPtr conf;
virConfValuePtr p;
driver->securityDefaultConfined = false;
driver->securityRequireConfined = false;
/* Set the container configuration directory */
if ((driver->configDir = strdup(LXC_CONFIG_DIR)) == NULL)
goto no_memory;
......@@ -161,14 +186,39 @@ int lxcLoadDriverConfig(lxc_driver_t *driver)
if (!conf)
goto done;
#define CHECK_TYPE(name,typ) if (p && p->type != (typ)) { \
lxcError(VIR_ERR_INTERNAL_ERROR, \
"%s: %s: expected type " #typ, \
filename, (name)); \
virConfFree(conf); \
return -1; \
}
p = virConfGetValue(conf, "log_with_libvirtd");
if (p) {
if (p->type != VIR_CONF_LONG)
VIR_WARN("lxcLoadDriverConfig: invalid setting: log_with_libvirtd");
else
driver->log_libvirtd = p->l;
CHECK_TYPE ("log_with_libvirtd", VIR_CONF_LONG);
if (p) driver->log_libvirtd = p->l;
p = virConfGetValue (conf, "security_driver");
CHECK_TYPE ("security_driver", VIR_CONF_STRING);
if (p && p->str) {
if (!(driver->securityDriverName = strdup(p->str))) {
virReportOOMError();
virConfFree(conf);
return -1;
}
}
p = virConfGetValue (conf, "security_default_confined");
CHECK_TYPE ("security_default_confined", VIR_CONF_LONG);
if (p) driver->securityDefaultConfined = p->l;
p = virConfGetValue (conf, "security_require_confined");
CHECK_TYPE ("security_require_confined", VIR_CONF_LONG);
if (p) driver->securityRequireConfined = p->l;
#undef CHECK_TYPE
virConfFree(conf);
done:
......
......@@ -33,6 +33,7 @@
# include "capabilities.h"
# include "threads.h"
# include "cgroup.h"
# include "security/security_manager.h"
# include "configmake.h"
# define LXC_CONFIG_DIR SYSCONFDIR "/libvirt/lxc"
......@@ -57,6 +58,11 @@ struct __lxc_driver {
virDomainEventStatePtr domainEventState;
char *securityDriverName;
bool securityDefaultConfined;
bool securityRequireConfined;
virSecurityManagerPtr securityManager;
/* Mapping of 'char *uuidstr' -> virConnectPtr
* of guests which will be automatically killed
* when the virConnectPtr is closed*/
......@@ -64,7 +70,7 @@ struct __lxc_driver {
};
int lxcLoadDriverConfig(lxc_driver_t *driver);
virCapsPtr lxcCapsInit(void);
virCapsPtr lxcCapsInit(lxc_driver_t *driver);
# define lxcError(code, ...) \
virReportErrorHelper(VIR_FROM_LXC, code, __FILE__, \
......
......@@ -91,6 +91,7 @@ typedef char lxc_message_t;
typedef struct __lxc_child_argv lxc_child_argv_t;
struct __lxc_child_argv {
virDomainDefPtr config;
virSecurityManagerPtr securityDriver;
unsigned int nveths;
char **veths;
int monitor;
......@@ -1297,6 +1298,10 @@ static int lxcContainerChild( void *data )
goto cleanup;
}
VIR_DEBUG("Setting up security labeling");
if (virSecurityManagerSetProcessLabel(argv->securityDriver, vmDef) < 0)
goto cleanup;
ret = 0;
cleanup:
VIR_FREE(ttyPath);
......@@ -1359,6 +1364,7 @@ const char *lxcContainerGetAlt32bitArch(const char *arch)
* Returns PID of container on success or -1 in case of error
*/
int lxcContainerStart(virDomainDefPtr def,
virSecurityManagerPtr securityDriver,
unsigned int nveths,
char **veths,
int control,
......@@ -1370,7 +1376,8 @@ int lxcContainerStart(virDomainDefPtr def,
int cflags;
int stacksize = getpagesize() * 4;
char *stack, *stacktop;
lxc_child_argv_t args = { def, nveths, veths, control,
lxc_child_argv_t args = { def, securityDriver,
nveths, veths, control,
ttyPaths, nttyPaths, handshakefd};
/* allocate a stack for the container */
......
......@@ -25,6 +25,7 @@
# define LXC_CONTAINER_H
# include "lxc_conf.h"
# include "security/security_manager.h"
enum {
LXC_CONTAINER_FEATURE_NET = (1 << 0),
......@@ -49,6 +50,7 @@ int lxcContainerSendContinue(int control);
int lxcContainerWaitForContinue(int control);
int lxcContainerStart(virDomainDefPtr def,
virSecurityManagerPtr securityDriver,
unsigned int nveths,
char **veths,
int control,
......
......@@ -1361,6 +1361,7 @@ cleanup:
static int
lxcControllerRun(virDomainDefPtr def,
virSecurityManagerPtr securityDriver,
unsigned int nveths,
char **veths,
int monitor,
......@@ -1515,6 +1516,7 @@ lxcControllerRun(virDomainDefPtr def,
goto cleanup;
if ((container = lxcContainerStart(def,
securityDriver,
nveths,
veths,
control[1],
......@@ -1623,11 +1625,13 @@ int main(int argc, char *argv[])
{ "veth", 1, NULL, 'v' },
{ "console", 1, NULL, 'c' },
{ "handshakefd", 1, NULL, 's' },
{ "security", 1, NULL, 'S' },
{ "help", 0, NULL, 'h' },
{ 0, 0, 0, 0 },
};
int *ttyFDs = NULL;
size_t nttyFDs = 0;
virSecurityManagerPtr securityDriver = NULL;
if (setlocale(LC_ALL, "") == NULL ||
bindtextdomain(PACKAGE, LOCALEDIR) == NULL ||
......@@ -1639,7 +1643,7 @@ int main(int argc, char *argv[])
while (1) {
int c;
c = getopt_long(argc, argv, "dn:v:m:c:s:h",
c = getopt_long(argc, argv, "dn:v:m:c:s:h:S:",
options, NULL);
if (c == -1)
......@@ -1687,6 +1691,14 @@ int main(int argc, char *argv[])
}
break;
case 'S':
if (!(securityDriver = virSecurityManagerNew(optarg, false, false, false))) {
fprintf(stderr, "Cannot create security manager '%s'",
optarg);
goto cleanup;
}
break;
case 'h':
case '?':
fprintf(stderr, "\n");
......@@ -1699,12 +1711,20 @@ int main(int argc, char *argv[])
fprintf(stderr, " -c FD, --console FD\n");
fprintf(stderr, " -v VETH, --veth VETH\n");
fprintf(stderr, " -s FD, --handshakefd FD\n");
fprintf(stderr, " -S NAME, --security NAME\n");
fprintf(stderr, " -h, --help\n");
fprintf(stderr, "\n");
goto cleanup;
}
}
if (securityDriver == NULL) {
if (!(securityDriver = virSecurityManagerNew("none", false, false, false))) {
fprintf(stderr, "%s: cannot initialize nop security manager", argv[0]);
goto cleanup;
}
}
if (name == NULL) {
fprintf(stderr, "%s: missing --name argument for configuration\n", argv[0]);
......@@ -1724,7 +1744,7 @@ int main(int argc, char *argv[])
virEventRegisterDefaultImpl();
if ((caps = lxcCapsInit()) == NULL)
if ((caps = lxcCapsInit(NULL)) == NULL)
goto cleanup;
if ((configFile = virDomainConfigFile(LXC_STATE_DIR,
......@@ -1790,10 +1810,10 @@ int main(int argc, char *argv[])
goto cleanup;
}
rc = lxcControllerRun(def, nveths, veths, monitor, client,
rc = lxcControllerRun(def, securityDriver,
nveths, veths, monitor, client,
ttyFDs, nttyFDs, handshakefd);
cleanup:
if (def)
virPidFileDelete(LXC_STATE_DIR, def->name);
......
......@@ -441,6 +441,9 @@ static virDomainPtr lxcDomainDefine(virConnectPtr conn, const char *xml)
VIR_DOMAIN_XML_INACTIVE)))
goto cleanup;
if (virSecurityManagerVerify(driver->securityManager, def) < 0)
goto cleanup;
if ((dupVM = virDomainObjIsDuplicate(&driver->domains, def, 0)) < 0)
goto cleanup;
......@@ -1394,7 +1397,21 @@ static int lxcMonitorClient(lxc_driver_t * driver,
return -1;
}
if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) {
if (virSecurityManagerSetSocketLabel(driver->securityManager, vm->def) < 0) {
VIR_ERROR(_("Failed to set security context for monitor for %s"),
vm->def->name);
goto error;
}
fd = socket(PF_UNIX, SOCK_STREAM, 0);
if (virSecurityManagerClearSocketLabel(driver->securityManager, vm->def) < 0) {
VIR_ERROR(_("Failed to clear security context for monitor for %s"),
vm->def->name);
goto error;
}
if (fd < 0) {
virReportSystemError(errno, "%s",
_("Failed to create client socket"));
goto error;
......@@ -1437,6 +1454,16 @@ static int lxcVmTerminate(lxc_driver_t *driver,
return -1;
}
virSecurityManagerRestoreAllLabel(driver->securityManager,
vm->def, false);
virSecurityManagerReleaseLabel(driver->securityManager, vm->def);
/* Clear out dynamically assigned labels */
if (vm->def->seclabel.type == VIR_DOMAIN_SECLABEL_DYNAMIC) {
VIR_FREE(vm->def->seclabel.model);
VIR_FREE(vm->def->seclabel.label);
VIR_FREE(vm->def->seclabel.imagelabel);
}
if (virCgroupForDomain(driver->cgroup, vm->def->name, &group, 0) == 0) {
rc = virCgroupKillPainfully(group);
if (rc < 0) {
......@@ -1567,6 +1594,10 @@ lxcBuildControllerCmd(lxc_driver_t *driver,
virCommandAddArgFormat(cmd, "%d", ttyFDs[i]);
virCommandPreserveFD(cmd, ttyFDs[i]);
}
if (driver->securityDriverName)
virCommandAddArgPair(cmd, "--security", driver->securityDriverName);
virCommandAddArg(cmd, "--handshake");
virCommandAddArgFormat(cmd, "%d", handshakefd);
virCommandAddArg(cmd, "--background");
......@@ -1761,6 +1792,24 @@ static int lxcVmStart(virConnectPtr conn,
virReportOOMError();
goto cleanup;
}
/* If you are using a SecurityDriver with dynamic labelling,
then generate a security label for isolation */
VIR_DEBUG("Generating domain security label (if required)");
if (vm->def->seclabel.type == VIR_DOMAIN_SECLABEL_DEFAULT)
vm->def->seclabel.type = VIR_DOMAIN_SECLABEL_NONE;
if (virSecurityManagerGenLabel(driver->securityManager, vm->def) < 0) {
virDomainAuditSecurityLabel(vm, false);
goto cleanup;
}
virDomainAuditSecurityLabel(vm, true);
VIR_DEBUG("Setting domain security labels");
if (virSecurityManagerSetAllLabel(driver->securityManager,
vm->def, NULL) < 0)
goto cleanup;
for (i = 0 ; i < vm->def->nconsoles ; i++)
ttyFDs[i] = -1;
......@@ -1916,6 +1965,16 @@ cleanup:
if (rc != 0) {
VIR_FORCE_CLOSE(priv->monitor);
virDomainConfVMNWFilterTeardown(vm);
virSecurityManagerRestoreAllLabel(driver->securityManager,
vm->def, false);
virSecurityManagerReleaseLabel(driver->securityManager, vm->def);
/* Clear out dynamically assigned labels */
if (vm->def->seclabel.type == VIR_DOMAIN_SECLABEL_DYNAMIC) {
VIR_FREE(vm->def->seclabel.model);
VIR_FREE(vm->def->seclabel.label);
VIR_FREE(vm->def->seclabel.imagelabel);
}
}
for (i = 0 ; i < nttyFDs ; i++)
VIR_FORCE_CLOSE(ttyFDs[i]);
......@@ -2040,6 +2099,9 @@ lxcDomainCreateAndStart(virConnectPtr conn,
VIR_DOMAIN_XML_INACTIVE)))
goto cleanup;
if (virSecurityManagerVerify(driver->securityManager, def) < 0)
goto cleanup;
if (virDomainObjIsDuplicate(&driver->domains, def, 1) < 0)
goto cleanup;
......@@ -2084,6 +2146,102 @@ cleanup:
}
static int lxcDomainGetSecurityLabel(virDomainPtr dom, virSecurityLabelPtr seclabel)
{
lxc_driver_t *driver = dom->conn->privateData;
virDomainObjPtr vm;
int ret = -1;
lxcDriverLock(driver);
vm = virDomainFindByUUID(&driver->domains, dom->uuid);
memset(seclabel, 0, sizeof(*seclabel));
if (!vm) {
char uuidstr[VIR_UUID_STRING_BUFLEN];
virUUIDFormat(dom->uuid, uuidstr);
lxcError(VIR_ERR_NO_DOMAIN,
_("no domain with matching uuid '%s'"), uuidstr);
goto cleanup;
}
if (!virDomainVirtTypeToString(vm->def->virtType)) {
lxcError(VIR_ERR_INTERNAL_ERROR,
_("unknown virt type in domain definition '%d'"),
vm->def->virtType);
goto cleanup;
}
/*
* Theoretically, the pid can be replaced during this operation and
* return the label of a different process. If atomicity is needed,
* further validation will be required.
*
* Comment from Dan Berrange:
*
* Well the PID as stored in the virDomainObjPtr can't be changed
* because you've got a locked object. The OS level PID could have
* exited, though and in extreme circumstances have cycled through all
* PIDs back to ours. We could sanity check that our PID still exists
* after reading the label, by checking that our FD connecting to the
* LXC monitor hasn't seen SIGHUP/ERR on poll().
*/
if (virDomainObjIsActive(vm)) {
if (virSecurityManagerGetProcessLabel(driver->securityManager,
vm->def, vm->pid, seclabel) < 0) {
lxcError(VIR_ERR_INTERNAL_ERROR,
"%s", _("Failed to get security label"));
goto cleanup;
}
}
ret = 0;
cleanup:
if (vm)
virDomainObjUnlock(vm);
lxcDriverUnlock(driver);
return ret;
}
static int lxcNodeGetSecurityModel(virConnectPtr conn,
virSecurityModelPtr secmodel)
{
lxc_driver_t *driver = conn->privateData;
int ret = 0;
lxcDriverLock(driver);
memset(secmodel, 0, sizeof(*secmodel));
/* NULL indicates no driver, which we treat as
* success, but simply return no data in *secmodel */
if (driver->caps->host.secModel.model == NULL)
goto cleanup;
if (!virStrcpy(secmodel->model, driver->caps->host.secModel.model,
VIR_SECURITY_MODEL_BUFLEN)) {
lxcError(VIR_ERR_INTERNAL_ERROR,
_("security model string exceeds max %d bytes"),
VIR_SECURITY_MODEL_BUFLEN - 1);
ret = -1;
goto cleanup;
}
if (!virStrcpy(secmodel->doi, driver->caps->host.secModel.doi,
VIR_SECURITY_DOI_BUFLEN)) {
lxcError(VIR_ERR_INTERNAL_ERROR,
_("security DOI string exceeds max %d bytes"),
VIR_SECURITY_DOI_BUFLEN-1);
ret = -1;
goto cleanup;
}
cleanup:
lxcDriverUnlock(driver);
return ret;
}
static int
lxcDomainEventRegister(virConnectPtr conn,
virConnectDomainEventCallback callback,
......@@ -2332,6 +2490,10 @@ lxcReconnectVM(void *payload, const void *name ATTRIBUTE_UNUSED, void *opaque)
lxcMonitorEvent,
vm, NULL)) < 0)
goto error;
if (virSecurityManagerReserveLabel(driver->securityManager,
vm->def, vm->pid) < 0)
goto error;
} else {
vm->def->id = -1;
VIR_FORCE_CLOSE(priv->monitor);
......@@ -2348,6 +2510,27 @@ error:
}
static int
lxcSecurityInit(lxc_driver_t *driver)
{
virSecurityManagerPtr mgr = virSecurityManagerNew(driver->securityDriverName,
false,
driver->securityDefaultConfined,
driver->securityRequireConfined);
if (!mgr)
goto error;
driver->securityManager = mgr;
return 0;
error:
VIR_ERROR(_("Failed to initialize security drivers"));
virSecurityManagerFree(mgr);
return -1;
}
static int lxcStartup(int privileged)
{
char *ld;
......@@ -2408,7 +2591,10 @@ static int lxcStartup(int privileged)
if (lxcLoadDriverConfig(lxc_driver) < 0)
goto cleanup;
if ((lxc_driver->caps = lxcCapsInit()) == NULL)
if (lxcSecurityInit(lxc_driver) < 0)
goto cleanup;
if ((lxc_driver->caps = lxcCapsInit(lxc_driver)) == NULL)
goto cleanup;
lxc_driver->caps->privateDataAllocFunc = lxcDomainObjPrivateAlloc;
......@@ -2500,6 +2686,7 @@ static int lxcShutdown(void)
lxcProcessAutoDestroyShutdown(lxc_driver);
virCapabilitiesFree(lxc_driver->caps);
virSecurityManagerFree(lxc_driver->securityManager);
VIR_FREE(lxc_driver->configDir);
VIR_FREE(lxc_driver->autostartDir);
VIR_FREE(lxc_driver->stateDir);
......@@ -3671,6 +3858,8 @@ static virDriver lxcDriver = {
.domainGetBlkioParameters = lxcDomainGetBlkioParameters, /* 0.9.8 */
.domainGetInfo = lxcDomainGetInfo, /* 0.4.2 */
.domainGetState = lxcDomainGetState, /* 0.9.2 */
.domainGetSecurityLabel = lxcDomainGetSecurityLabel, /* 0.9.10 */
.nodeGetSecurityModel = lxcNodeGetSecurityModel, /* 0.9.10 */
.domainGetXMLDesc = lxcDomainGetXMLDesc, /* 0.4.2 */
.listDefinedDomains = lxcListDefinedDomains, /* 0.4.2 */
.numOfDefinedDomains = lxcNumDefinedDomains, /* 0.4.2 */
......
......@@ -13,6 +13,7 @@ module Test_libvirtd_lxc =
# This is disabled by default, uncomment below to enable it.
#
log_with_libvirtd = 1
security_driver = \"selinux\"
"
test Libvirtd_lxc.lns get conf =
......@@ -29,3 +30,4 @@ log_with_libvirtd = 1
{ "#comment" = "This is disabled by default, uncomment below to enable it." }
{ "#comment" = "" }
{ "log_with_libvirtd" = "1" }
{ "security_driver" = "selinux" }
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册